)]}'
{
  "log": [
    {
      "commit": "94e3cdf35d120c4d869851714a2cf9f582e9b61e",
      "tree": "8f3cbc155845f3c9e43e968c1ac9711848dfcf25",
      "parents": [
        "8a7a9bb9e5dbd2c5ed5351ecdd8216effd3327f4",
        "c948840051b34f2e93cec340f2ecd901a8c07979"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Sun Jun 28 11:13:48 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 28 11:13:48 2026 -0400"
      },
      "message": "Merge pull request #340 from artgins/exact-alg-pin-verify\n\njwt: pin the exact algorithm in both verify paths (RFC 8725 3.1)"
    },
    {
      "commit": "c948840051b34f2e93cec340f2ecd901a8c07979",
      "tree": "8f3cbc155845f3c9e43e968c1ac9711848dfcf25",
      "parents": [
        "8a7a9bb9e5dbd2c5ed5351ecdd8216effd3327f4"
      ],
      "author": {
        "name": "Core",
        "email": "core@artgins.com",
        "time": "Sun Jun 28 16:16:55 2026 +0200"
      },
      "committer": {
        "name": "Core",
        "email": "core@artgins.com",
        "time": "Sun Jun 28 16:21:56 2026 +0200"
      },
      "message": "jwt: pin the exact algorithm in both verify paths (RFC 8725 3.1)\n\nOn the pinned path of __verify_config_post() (Compact) -- both config-\u003ealg and\nconfig-\u003ekey-\u003ealg set and equal -- the alg-vs-alg checks compare config and key\nto each OTHER, but none of them compares the token\u0027s own alg (jwt-\u003ealg) against\nthe pinned alg. The jwt_alg_required_kty() backstop that follows is only\nfamily-granular (every RSn/PSn alg maps to JWK_KEY_TYPE_RSA, every ESn to EC).\nThe effect: a token presenting \"RS512\" verifies against a key the caller pinned\nas \"RS256\" -- same family, valid signature -- so a pinned verifier accepts an\nalgorithm it never pinned.\n\nThe JSON Serialization path has the same gap in try_candidate(): it gates the\ncandidate key by jwt_alg_required_kty(s-\u003ealg) (family-granular) but, for the\nexplicit-key and \"kid\"-matched branches, never checks the key\u0027s own declared\nalg against the signature alg. (The keyring-scan branch already skips on\n\"kalg !\u003d s-\u003ealg\"; the single-key and kid branches did not.)\n\nThis is not a forgery (the signature must still validate under the key), but it\nis a policy/conformance gap: RFC 8725 3.1 calls for using algorithm\nidentifiers, not just key types, in the verification decision.\n\n  - Compact: require jwt-\u003ealg to equal whichever alg the caller pinned\n    (config-\u003ealg, or config-\u003ekey-\u003ealg when only the key carries one), right\n    before the kty backstop.\n  - JSON: in try_candidate() -- the single choke point for all three key-\n    selection branches -- skip a candidate whose declared \"alg\" differs from\n    the signature alg, mirroring the scan branch\u0027s existing guard.\n\nBoth are purely additive: no legitimately-pinned token is newly rejected.\n\ntests/jwt_alg_downgrade.c (every crypto backend, both Compact and Flattened\nJSON serializations): an RS512 token verified against an RS256-pinned key is\nnow rejected on both paths; an RS256 token under an RS256 pin, and an RS512\ntoken under a legitimate RS512 pin, both still verify. Adds\ntests/keys/rsa_key_2048_no_alg.json (the existing 2048-bit RSA modulus with no\n\"alg\" hint, so the builder can sign any RSA alg on it). Full suite 40/40\nlocally (OpenSSL + Jansson).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Core \u003ccore@artgins.com\u003e\n"
    },
    {
      "commit": "8a7a9bb9e5dbd2c5ed5351ecdd8216effd3327f4",
      "tree": "6800816c13268ae7f218f2537457dd1f8cd519dd",
      "parents": [
        "f4edfe5b2c44ac57edf00342550e81017f5329eb"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Fri Jun 19 10:59:40 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Fri Jun 19 10:59:40 2026 -0400"
      },
      "message": "Add curl to combo tests and memcheck\n\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "f4edfe5b2c44ac57edf00342550e81017f5329eb",
      "tree": "1eb1210f0ffdf79222855e29698b13db792a355c",
      "parents": [
        "ca975face278ae5e0a499276826ff04fc21fb52c",
        "197ac5881a941d8cd099721fc3144f7dac182776"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Fri Jun 19 09:42:13 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Jun 19 09:42:13 2026 -0400"
      },
      "message": "Merge pull request #338 from benmcollins/release-3.6.1\n\nRelease v3.6.1"
    },
    {
      "commit": "197ac5881a941d8cd099721fc3144f7dac182776",
      "tree": "1eb1210f0ffdf79222855e29698b13db792a355c",
      "parents": [
        "ca975face278ae5e0a499276826ff04fc21fb52c"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Fri Jun 19 08:49:54 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Fri Jun 19 09:20:43 2026 -0400"
      },
      "message": "Release v3.6.1\n\nBump LIBJWT_VERSION_SET 3 6 0 -\u003e 3 6 1.\n\nSONAME: 18:0:4 -\u003e 18:1:4 (revision++, current/age untouched). The only change\nsince v3.6.0 is a test-portability fix (assert write() in the jwks-cache test\nserver instead of a (void) cast, which newer glibc rejects under -Werror with\nwarn_unused_result). No library source changed, so the exported jwt*/jwk*/jwe*\nsymbol set is identical to v3.6.0 and per the libtool rules only the SONAME\nrevision advances. The SONAME major (current - age) stays 14.\n\nmake check passes.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "ca975face278ae5e0a499276826ff04fc21fb52c",
      "tree": "f7dc9e8eb853183c3a1b5598038de8d9abc0adf5",
      "parents": [
        "b7bb97a637c94ec50832daf40b954c5cd08cc964"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Fri Jun 19 09:20:22 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Fri Jun 19 09:20:22 2026 -0400"
      },
      "message": "tests: consume write() result without asserting in the jwks test server\n\nThe previous fix used ck_assert_int_gt(write(...), 0), but that is unsafe in the\nin-process HTTP server: it runs on a worker thread, and (a) a test client may\nlegitimately hang up early so write() can return -1/short without it being a\nbug, and (b) a failing Check assertion longjmp()s to the main thread\u0027s setjmp,\na cross-thread jump that corrupts unwinding and leaks the worker thread\u0027s TLS\n(valgrind: ~49 errors, breaking the memcheck CI row).\n\nConsume the result into a variable instead and (void) it: this satisfies glibc\u0027s\nwarn_unused_result under -Werror (the original problem) and is valgrind-clean.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "b7bb97a637c94ec50832daf40b954c5cd08cc964",
      "tree": "f171a05ab2b0733c656af9b8e33edc1bfba6a527",
      "parents": [
        "273d52267ddf83aa5d39f52333d0504bc6986ce3"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Fri Jun 19 08:48:43 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Fri Jun 19 08:48:43 2026 -0400"
      },
      "message": "tests: check write() return in the jwks-cache test server\n\nThe in-process HTTP server cast write() to (void) to ignore its result, but\nglibc marks write() __attribute__((warn_unused_result)) and a (void) cast does\nnot suppress it on newer glibc, so the -Werror build fails. Assert the result\ninstead with ck_assert_int_gt(write(...), 0).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "273d52267ddf83aa5d39f52333d0504bc6986ce3",
      "tree": "2c4a5305fa489c23752340b68ea361199fb955e4",
      "parents": [
        "791fe9dee8288fc0b3af5dcf69f5e979e3faa209",
        "8fb746457832fb2b17808c647625849947129ed0"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Fri Jun 19 00:08:44 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Jun 19 00:08:44 2026 -0400"
      },
      "message": "Merge pull request #337 from benmcollins/release-3.6.0\n\nRelease v3.6.0"
    },
    {
      "commit": "8fb746457832fb2b17808c647625849947129ed0",
      "tree": "2c4a5305fa489c23752340b68ea361199fb955e4",
      "parents": [
        "791fe9dee8288fc0b3af5dcf69f5e979e3faa209"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 23:46:49 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 23:46:49 2026 -0400"
      },
      "message": "Release v3.6.0\n\nBump LIBJWT_VERSION_SET 3 5 0 -\u003e 3 6 0.\n\nSONAME: 17:1:3 -\u003e 18:0:4 (current++, revision\u003d0, age++). An nm diff of the\nexported jwt*/jwk*/jwe* symbols between v3.5.0 and HEAD shows 37 symbols added\nand none removed or changed, which per the libtool rules is a current++,\nrevision\u003d0, age++ bump. The SONAME major (current - age) stays 14, unchanged\nacross all of 3.x.\n\nThe 37 new symbols span this cycle\u0027s work: RFC 7638/9278 JWK thumbprints\n(jwks_item_thumbprint[_uri], jwks_find_bythumbprint[_uri]), RFC 7800 cnf helpers\n(jwt_builder_setcnf*, jwt_get_cnf), JWS JSON Serialization / multi-signature\n(jwt_builder_set_format, add_signature, jwt_signature_add_*_json,\njwt_checker_setkeyring, sig_count/sig_verified/sig_key), RFC 7797 unencoded /\ndetached payloads (jwt_builder_setpayload/setb64/set_detached,\njwt_checker_verify_detached), RFC 8725 typ + algorithm allowlist\n(jwt_builder_settyp, jwt_checker_expect_typ/setalgs), key generation\n(jwks_generate, jwks_create_generate), AES-GCM-KW + PBES2 JWE\n(jwe_builder_setpbes2), RFC 7517 X.509 params (jwks_item_x5c[_count]/x5t[_s256]),\nthe cached remote JWKS source (jwks_load_fromurl_cached, jwks_refresh_fromurl),\nand the application-profile primitives (jwt_checker_require,\njwt_checker_enable_embedded_jwk[_keyring], jwt_token_hash[_half]). New enum\nvalues (JWE_ALG_*GCMKW / *PBES2*, jwk_thumbprint_alg_t, jwt_serialization_t,\njwt_verify_policy_t) and the LIBJWT_HAVE_ML_DSA path carry @since 3.6.0 but are\nnot linker symbols, so they do not move current/age.\n\nmake check passes; valgrind clean.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "791fe9dee8288fc0b3af5dcf69f5e979e3faa209",
      "tree": "93058f66e61ac049f58bd1d0e26b083b2ed3155b",
      "parents": [
        "d0760dc7d1ea8f547e28a81e69e8c33832f73ccd",
        "929bd6887a03370e381198042860f6d6fece3b88"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 23:42:14 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 23:42:14 2026 -0400"
      },
      "message": "Merge pull request #336 from benmcollins/317-app-profiles\n\njwt: application profiles + the primitives they need (#317)"
    },
    {
      "commit": "929bd6887a03370e381198042860f6d6fece3b88",
      "tree": "93058f66e61ac049f58bd1d0e26b083b2ed3155b",
      "parents": [
        "657921590fc9cf7eda871d2bda1bcae7471ba1c3"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 22:58:23 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 22:58:23 2026 -0400"
      },
      "message": "jwt: enforce jwt_checker_require() on the JWS JSON Serialization path\n\n__verify_required() was only called from __verify_config_post() (the Compact\npath), so a token presented in the JWS JSON Serialization bypassed the\nrequired-claims-present policy entirely — a verifier-policy bypass. Call it in\njwt_verify_json() alongside the existing __verify_claims() check (same jwt-\u003eb64\ngate), propagating the specific \"missing claim\" error with jwt_copy_error().\n\nCovered by a new test_require_json_serialization (a Flattened JSON token with a\npresent claim verifies; a missing required claim is rejected).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "657921590fc9cf7eda871d2bda1bcae7471ba1c3",
      "tree": "5c2d274b42d91ea5b61c828058c3f548f043c370",
      "parents": [
        "d0760dc7d1ea8f547e28a81e69e8c33832f73ccd"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 22:53:42 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 22:53:42 2026 -0400"
      },
      "message": "jwt: application profiles + the primitives they need (#317)\n\nThe real-world JWT profiles (at+jwt, VAPID, PASSporT, OpenID4VCI, DPoP, mTLS,\nJAdES) are application typ+claim conventions over signed JWTs, not new crypto.\nExpose the three small primitives that were genuinely missing, then document and\ntest each profile as a one-call recipe over the public API.\n\nNew primitives (all @since 3.6.0):\n\n  - jwt_checker_require(checker, names, count): assert a set of claims is PRESENT\n    independent of value (RFC 9068 mandates presence; LibJWT otherwise tolerates\n    an absent claim). Checked in __verify_required() alongside the value checks.\n\n  - jwt_checker_enable_embedded_jwk(checker, alg, expected_jkt) and the keyring\n    variant: verify a self-contained token whose key is the protected-header\n    \"jwk\" (RFC 7515 4.1.3) -- the DPoP / OpenID4VCI mechanism. The header key is\n    attacker-supplied, so it is confirmed against a pinned thumbprint (or an\n    allowlist via jwks_find_bythumbprint) before use, and still passes the normal\n    kty/alg anti-confusion gate; a NULL/empty pin fails the enable call. The\n    confirmed key is parsed into a checker-owned keyring (reset per verify) so\n    jwt_checker_sig_key() can borrow it. Scoped to the Compact Serialization,\n    which is what DPoP/OpenID4VCI use.\n\n  - jwt_token_hash(value, alg) \u003d base64url(SHA-x(value)) over the full digest\n    (DPoP \"ath\"), and jwt_token_hash_half(value, alg) \u003d the left half keyed to\n    the JWS alg (OIDC at_hash/c_hash). The OIDC Core 3.1.3.6 at_hash example is a\n    test vector.\n\njwk_thumbprint_alg_t moves earlier in jwt.h (it now also selects the embedded-JWK\npin hash and the token hash); it is grouped under jwt_alg_grp so @ref resolves.\n\nTests: jwt_hash.c (vectors + widths), jwt_require.c, jwt_embedded_jwk.c (pin +\nkeyring confirm/reject, signature-must-match-embedded, malformed-key fuzz,\nchecker reuse), and the capstone jwt_profiles.c (all seven profiles build+verify\nend to end). 40/40 suites pass under both JSON backends and all three crypto\nbackends in the CI image; valgrind-clean; no new uncovered lines.\n\nDocs: a new doxygen/profiles.dox \"Application Profiles\" page with a worked recipe\nper profile, linked from the mainpage; README + standards table updated.\n\nCloses #317\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "d0760dc7d1ea8f547e28a81e69e8c33832f73ccd",
      "tree": "16557b1713c7aa1402066a3e97ccb920fdcd4701",
      "parents": [
        "77f4141032afdb3709c4ab32cf3781398fa95826",
        "22a089aada723ab9006471ba0fdf55f423036cd3"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 21:44:04 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 21:44:04 2026 -0400"
      },
      "message": "Merge pull request #335 from benmcollins/313-jwks-cache\n\njwks: cached/refreshing remote JWKS source (TTL/ETag/cooldown, #313)"
    },
    {
      "commit": "22a089aada723ab9006471ba0fdf55f423036cd3",
      "tree": "16557b1713c7aa1402066a3e97ccb920fdcd4701",
      "parents": [
        "6f2379672aafeca8569c49be3a7f232af5350d1e"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 18:55:16 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 18:55:16 2026 -0400"
      },
      "message": "jwks: harden the cached source against HTTP errors and failing refreshes\n\nAdversarial review of the cache found three real defects; fix all three:\n\n  - HTTP errors wiped the cache. cache_apply() special-cased only 304 and\n    replaced the keys for every other status. __curl_fetch() returns success\n    for any completed HTTP exchange (a 4xx/5xx, or an unfollowed 3xx), so a\n    transient origin/proxy error destroyed a good keyring and then marked the\n    empty set fresh. Now only a 2xx (with a body that parses to a usable JWKS)\n    replaces the keys; 304 keeps them; any other status keeps the previously\n    cached keys and sets an error -- matching the documented contract.\n\n  - Cooldown did not bound FAILING refreshes. last_fetch was stamped only on a\n    completed fetch (in cache_apply), so while the JWKS host was unreachable a\n    kid-miss refresh was never throttled -- an unbounded outbound-request\n    amplifier. last_fetch is now stamped before every attempt, so a failed\n    attempt still consumes the cooldown window.\n\n  - A server-controlled Cache-Control max-age could overflow time_t in the\n    expiry computation. The lifetime (server max-age or configured TTL) is now\n    clamped to JWKS_MAX_TTL (one week).\n\ntests/jwt_jwks_cache.c: new test asserting a 5xx during a forced refresh\nretains the cached keys and reports the error. 36/36 suites pass; valgrind-clean.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "6f2379672aafeca8569c49be3a7f232af5350d1e",
      "tree": "ada4dc21e3603e2a03734e42b9699a9b0c22e973",
      "parents": [
        "77f4141032afdb3709c4ab32cf3781398fa95826"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 18:46:39 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 18:46:39 2026 -0400"
      },
      "message": "jwks: add a cached/refreshing remote JWKS source (RFC 7517, #313)\n\nAdd jwks_load_fromurl_cached() + jwks_refresh_fromurl(): a caching wrapper around\nthe libcurl fetch that turns the existing one-shot jwks_load_fromurl() into a\nreusable JWKS source.\n\n  - TTL: the keyring is served from cache with no network request while fresh.\n    Freshness comes from the response Cache-Control: max-age, else the configured\n    jwks_url_config_t.ttl (default 5 min).\n  - Conditional refresh: once stale, a conditional GET (If-None-Match with the\n    stored ETag) refreshes the cache; a 304 Not Modified keeps the existing keys.\n  - kid rotation: jwks_refresh_fromurl() forces a refresh (for an unknown-kid\n    miss) even while fresh.\n  - Cooldown (load-bearing anti-DoS): the forced refresh is rate-limited (default\n    60 s) so an attacker presenting random kids cannot amplify into a flood of\n    outbound fetches.\n  - SSRF guard: only http(s) URLs are accepted by the cached path (unlike the\n    one-shot loader, which still allows file://).\n\nThe fetch path now parses Cache-Control/ETag (CURLOPT_HEADERFUNCTION), captures\nthe HTTP status, and sends conditional requests; curl_global_init() is done once\nrather than per request. The cache lives on the jwk_set and is freed with it.\nWithout libcurl the two new calls are clean no-ops.\n\ntests/jwt_jwks_cache.c: an in-process threaded HTTP server (counting requests,\nserving Cache-Control/ETag and 304) verifies the cache hit (no request), the\nTTL-stale conditional GET (304 keeps keys), the cooldown (a kid-miss refresh is\nbounded), and the http(s) SSRF guard. Gated on WITH_LIBCURL (a clean skip\notherwise). 36/36 suites pass under both JSON backends and in the CI image;\nvalgrind-clean. @since 3.6.0; README updated.\n\nPhase-2 work (jku/x5u fetching sharing this allowlist) remains future.\n\nCloses #313\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "77f4141032afdb3709c4ab32cf3781398fa95826",
      "tree": "89b2a350b5943575307eebb68ea0e1942e5c3dd5",
      "parents": [
        "329caad10c7c45bbb02b70e16ec17f4119ac12ec"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 18:01:28 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 18:01:28 2026 -0400"
      },
      "message": "ci: run the \"all\" row valgrind/memcheck only on PRs, not on master pushes\n\nThe Memcheck (ctest -T memcheck) step in the comprehensive \"all\" combo row is\nthe slowest part of CI. Gate it on github.event_name \u003d\u003d \u0027pull_request\u0027 so it\nruns on pull requests -- catching leaks before merge -- but is skipped on the\npush-to-master event, where the same commit already passed it as a PR. Coverage\n-\u003e Codecov still runs on both.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "329caad10c7c45bbb02b70e16ec17f4119ac12ec",
      "tree": "80f4f21bd3f2653b744fbbb639bc9aa3c15e42c2",
      "parents": [
        "171100db169aabeba732961b6984888c337d5522",
        "c222fb48823cab3b3e8235f68ca6bb8dddfa4040"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 15:54:33 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 15:54:33 2026 -0400"
      },
      "message": "Merge pull request #334 from benmcollins/314-x509-phase1\n\njwks: X.509 JWK params x5c/x5t/x5t#S256 (RFC 7517, #314 phase 1)"
    },
    {
      "commit": "c222fb48823cab3b3e8235f68ca6bb8dddfa4040",
      "tree": "80f4f21bd3f2653b744fbbb639bc9aa3c15e42c2",
      "parents": [
        "171100db169aabeba732961b6984888c337d5522"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 11:57:35 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 11:57:35 2026 -0400"
      },
      "message": "jwks: parse and expose X.509 params x5c/x5t/x5t#S256 (RFC 7517, #314 phase 1)\n\nParse a JWK\u0027s X.509 parameters and expose them; phase 1 is parse + expose only,\nwith chain validation and x5u fetching deliberately left to a later phase (both\nare security-critical: trust-anchor policy and SSRF).\n\n  - jwks_item_x5c_count() / jwks_item_x5c(i, \u0026len): the \"x5c\" certificate chain,\n    decoded to DER (standard base64 via base64_decode, now exported from jwt.c),\n    leaf at index 0. The DER is owned by the item; the caller validates the chain.\n  - jwks_item_x5t() / jwks_item_x5t_s256(): the \"x5t\" / \"x5t#S256\" thumbprints.\n\nAt parse, when a JWK has both \"x5c\" and \"x5t#S256\", the thumbprint MUST equal\nbase64url(SHA-256(DER(leaf))) (RFC 7517 4.9, reusing the RFC 7638 sha op); a\nmismatch marks the item in error. The legacy SHA-1 \"x5t\" is exposed verbatim and\nnot recomputed (the sha op is SHA-2 only, and SHA-1 is deprecated).\n\ntests/jwt_x509.c (both JSON backends, all crypto backends): x5c parses to DER,\nthe leaf is real DER, out-of-range returns NULL, x5t#S256 present; a JWK whose\nx5t#S256 disagrees with its x5c leaf is rejected; a JWK with no X.509 params\nexposes none. Fixtures generated from a self-signed cert. 35/35 suites pass\nlocally and in the CI image; valgrind-clean. @since 3.6.0; README updated.\n\nCloses #314\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "171100db169aabeba732961b6984888c337d5522",
      "tree": "b1fd0ab7ade321f36337ca0cb3533958b337fde6",
      "parents": [
        "92615bad4bb81d01b31e8be530f2b61d71f97fe5",
        "9cd2114d991e8fab6f82c6d59129fc3f6a07ac01"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 11:48:28 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 11:48:28 2026 -0400"
      },
      "message": "Merge pull request #333 from benmcollins/310-pbes2\n\njwe: PBES2 password-based key management (RFC 7518 4.8)"
    },
    {
      "commit": "9cd2114d991e8fab6f82c6d59129fc3f6a07ac01",
      "tree": "b1fd0ab7ade321f36337ca0cb3533958b337fde6",
      "parents": [
        "92615bad4bb81d01b31e8be530f2b61d71f97fe5"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 11:28:30 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 11:28:30 2026 -0400"
      },
      "message": "jwe: add PBES2 password-based key management (RFC 7518 4.8)\n\nAdd PBES2-HS256+A128KW / PBES2-HS384+A192KW / PBES2-HS512+A256KW: a key-wrapping\nkey is derived from a passphrase (the oct key\u0027s octets) with PBKDF2 over a fresh\nrandom salt, then AES-KW wraps the CEK. The \"p2s\" (salt) and \"p2c\" (iterations)\ntravel in the per-recipient header (the protected header for Compact), threaded\nlike the GCM-KW iv/tag and ECDH-ES epk.\n\nNew per-backend PBKDF2 crypto op (there was none): OpenSSL PKCS5_PBKDF2_HMAC,\nGnuTLS gnutls_pbkdf2, MbedTLS via PSA on 4.x and the public PKCS#5 API on 3.6.x\n(the 3.6.x PSA build in the CI image does not enable PSA PBKDF2 at runtime, and\nmbedtls/pkcs5.h is public in 3.6.x but private in 4.x) -- gated on\nMBEDTLS_VERSION_MAJOR. The derivation follows RFC 7518 4.8.1: the PBKDF2 salt is\n(UTF8(alg) || 0x00 || p2s). jwe_builder_setpbes2() sets the iteration count\n(0 \u003d a strong default).\n\nSecurity (RFC 7518 8.8): the salt is generated internally per wrap and never\ncaller-supplied; on decrypt the attacker-controlled p2c is capped (hard maximum)\nand the salt length floored (\u003e\u003d 8 octets) BEFORE any PBKDF2 work, blocking a\nCPU-exhaustion DoS. A bad password / over-cap p2c / short salt funnels to a\nrandom CEK so the content AEAD fails uniformly (RFC 7516 11.5).\n\ntests/jwe_pbes2.c (both JSON backends, all crypto backends): round-trip all\nthree variants (Compact + JSON), over-cap p2c rejected, zero p2c rejected, short\nsalt rejected, wrong password rejected. 34/34 suites pass locally (MbedTLS 4.1,\nPSA path) and in the CI image (MbedTLS 3.6.6, PKCS#5 path); valgrind-clean.\n@since 3.6.0 on the new enum values and jwe_builder_setpbes2; README JWE matrix +\np2c-cap note added.\n\nCloses #310\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "92615bad4bb81d01b31e8be530f2b61d71f97fe5",
      "tree": "85f9ba16c30530e34112c8a0b4b86b7bae9fc0e5",
      "parents": [
        "a02ce8ee8c49d573b050df01c895e8cd588cea4b"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 11:03:23 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 11:03:23 2026 -0400"
      },
      "message": "tests: fix jwe_multi key-set leaks (reusable builder references keys)\n\nThe JWE builder references the jwk_item handed to setkey/add_recipient (it does\nnot copy it), so a key must outlive jwe_builder_generate(). Several multi-\nrecipient tests loaded a recipient key via the global read_json() slot and then\ncalled read_json() (or decrypt_ok(), which does) again, orphaning the first\njwk_set -- valgrind reported ~5 KB definitely lost across these tests.\n\nLoad each concurrently-referenced key into its own jwk_set_auto_t (the\nconvention ks_rsa/ks_ec already use), so it is freed at scope end (after\ngenerate) rather than leaked. No library change. jwe_multi is now valgrind-clean\n(0 bytes lost, 30/30 checks) across all backends.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "a02ce8ee8c49d573b050df01c895e8cd588cea4b",
      "tree": "a1c59d226d49e5d6c7306a0a9dd90e12bff9bfc5",
      "parents": [
        "37cf1efe79d91a62c4ccc73b87865b27ca5a7bdf",
        "6ef66d28c64bae5001e860cd103c07a8f5b38e8f"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 10:55:33 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 10:55:33 2026 -0400"
      },
      "message": "Merge pull request #332 from benmcollins/309-aes-gcm-kw\n\njwe: AES-GCM Key Wrap (A128/192/256GCMKW, RFC 7518 4.7)"
    },
    {
      "commit": "6ef66d28c64bae5001e860cd103c07a8f5b38e8f",
      "tree": "a1c59d226d49e5d6c7306a0a9dd90e12bff9bfc5",
      "parents": [
        "37cf1efe79d91a62c4ccc73b87865b27ca5a7bdf"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 10:18:49 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 10:18:49 2026 -0400"
      },
      "message": "jwe: add AES-GCM Key Wrap (A128/192/256GCMKW, RFC 7518 4.7)\n\nAdd the GCM-based key-management algorithms A128GCMKW/A192GCMKW/A256GCMKW: the\nCEK is wrapped by AES-GCM under the oct KEK with a fresh 96-bit IV and an empty\nAAD, and the resulting IV and authentication tag travel as the per-recipient\n\"iv\"/\"tag\" header parameters.\n\nNo new backend code: the wrap/unwrap reuse the existing content-encryption GCM\nops (A128GCMKW -\u003e A128GCM, etc., chosen by KEK size). jwe_gcmkw_wrap/unwrap\n(jwe.c) generate the IV and write/read \"iv\"/\"tag\" through the key-management\nheader, which jwe-common.c already routes to the protected header (Compact) or\nthe per-recipient header (JSON) -- exactly as it threads the ECDH-ES \"epk\". A\nfresh CSPRNG IV per wrap is generated internally and is never caller-supplied\n(RFC 7518 8.7: nonce reuse under one KEK is a catastrophic GCM break). On\ndecrypt a bad key-unwrap tag funnels to a random CEK so the content AEAD fails\nuniformly (RFC 7516 11.5). \"iv\"/\"tag\" join the reserved header names so an\napplication cannot override them.\n\nAlso fix a pre-existing leak so the JWE builder is reusable: wrap_recipient now\nfrees any Encrypted Key left from a previous jwe_builder_generate() before\nwrapping a recipient again (affected every wrapping alg, not just GCM-KW).\n\ntests/jwe_gcmkw.c (both JSON backends, all crypto backends): round-trip all\nthree sizes (Compact + Flattened + General JSON), GCM-KW wrapping a CBC-HMAC\nCEK, fresh-IV-on-reuse (reuses one builder), tamper -\u003e reject, wrong KEK length\n-\u003e reject. 33/33 suites pass locally (MbedTLS 4.1) and in the CI image\n(MbedTLS 3.6.6); valgrind-clean. @since 3.6.0 on the new enum values; README\nJWE key-management matrix updated.\n\nCloses #309\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "37cf1efe79d91a62c4ccc73b87865b27ca5a7bdf",
      "tree": "7c2b1a16a03b09ca8f2883dcfe5e20b79d1c2c39",
      "parents": [
        "406b84643da7f533c82539e640a552026b03d813",
        "8da8130a50fa413a725e49629ac451e9248dffb5"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 09:54:31 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 09:54:31 2026 -0400"
      },
      "message": "Merge pull request #331 from benmcollins/311-jwks-generate\n\njwks: jwks_generate() key-generation API (all backends + AKP)"
    },
    {
      "commit": "8da8130a50fa413a725e49629ac451e9248dffb5",
      "tree": "7c2b1a16a03b09ca8f2883dcfe5e20b79d1c2c39",
      "parents": [
        "a1bc8d30ccd070625577734b830330cee5738a29"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 09:28:49 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 09:28:49 2026 -0400"
      },
      "message": "mbedtls: scrub the private-key PEM stack buffer in generate_pem\n\nmbedtls_generate_pem() wrote a freshly generated unencrypted PKCS#8 private-key\nPEM into an 8192-byte stack buffer and returned without zeroizing it, leaving\nkey material in the stack frame. Scrub it with mbedtls_platform_zeroize() before\nreturning, matching the existing convention in set_pem_best_effort() in the same\nfile. Found by adversarial review (two independent reviewers).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "a1bc8d30ccd070625577734b830330cee5738a29",
      "tree": "9f630e30781ab79868faaf6b8959cfb006672311",
      "parents": [
        "406b84643da7f533c82539e640a552026b03d813"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 09:22:19 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 09:22:19 2026 -0400"
      },
      "message": "jwks: add jwks_generate() key-generation API (all backends)\n\nAdd a public key-generation API returning a fresh key as a jwk_item_t, the\nmost-cited ergonomic gap for a C JOSE library:\n\n  jwk_set_t *jwks_generate(jwk_set_t *set, jwk_key_type_t kty,\n                           const char *param, jwt_alg_t alg, unsigned int flags);\n  jwk_set_t *jwks_create_generate(...);  /* set \u003d NULL */\n\n@param is the key geometry (curve / RSA-or-oct bits; NULL \u003d default); @alg is\nthe JWA discriminator (RSA vs RSA-PSS, the ML-DSA variant, oct length intent);\nJWK_KEY_GEN_KID stamps the RFC 7638 thumbprint as the kid. The generated key is\nbound to the active backend, so it signs/verifies immediately.\n\nDesign (3-way design panel): a new crypto-op generate_pem() emits an unencrypted\nPKCS#8 private PEM per backend; common code runs it through the EXISTING\njwt_key2jwk -\u003e jwks_process_array pipeline, which sets provider_data and the JWK\nmembers for free. oct is generated in common code via jwt_ops-\u003erng (provider\u003dANY,\nworks on every backend). Coverage:\n  - OpenSSL: EC (incl. secp256k1), RSA, RSA-PSS, OKP (Ed/X), AKP (ML-DSA)\n  - GnuTLS:  EC (NIST), RSA, RSA-PSS, OKP (Ed25519/Ed448), AKP (ML-DSA); no\n             secp256k1 / X-curves (GnuTLS limitation)\n  - MbedTLS: EC (incl. secp256k1), RSA, RSA-PSS; no EdDSA / ML-DSA\nAn unsupported (kty, backend) pair returns a clean error (no crash); the\nalg\u003c-\u003ekty anti-confusion gate rejects mismatches; RSA bits floor 2048. The\ncaller\u0027s alg is recorded so PS384/PS512 (which the in-key PEM cannot encode) and\nthe ML-DSA variant survive.\n\ntests/jwt_generate.c (both JSON backends, all crypto backends): core types\n(EC/RSA/RSA-PSS/oct) must generate + round-trip sign/verify everywhere; optional\ntypes (secp256k1/OKP/AKP) are probed (verified where supported, clean error\notherwise); bad requests rejected; append-to-keyring; kid. 32/32 suites pass\nlocally (MbedTLS 4.1) and in the CI image (MbedTLS 3.6.6 + leancrypto);\nvalgrind-clean. @since 3.6.0; README updated.\n\nCloses #311\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "406b84643da7f533c82539e640a552026b03d813",
      "tree": "503f91e239d2d8ad3985ab82292dfe9d960416b0",
      "parents": [
        "da5c24cd5ce3a3ab1675a9ddf3bbcf54b7dcb420",
        "910e3e4a4693db46816c5dadd545fc38d24d6975"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 08:17:13 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 08:17:13 2026 -0400"
      },
      "message": "Merge pull request #330 from benmcollins/312-typ-allowlist\n\njwt: typ media-type helper + algorithm allowlist (RFC 8725)"
    },
    {
      "commit": "910e3e4a4693db46816c5dadd545fc38d24d6975",
      "tree": "503f91e239d2d8ad3985ab82292dfe9d960416b0",
      "parents": [
        "da5c24cd5ce3a3ab1675a9ddf3bbcf54b7dcb420"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 07:59:16 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 07:59:16 2026 -0400"
      },
      "message": "jwt: add typ media-type helper and algorithm allowlist (RFC 8725)\n\nTwo no-crypto checker ergonomics from RFC 8725 / BCP 225 that turn registered\nJWT profiles (at+jwt, dpop+jwt, secevent+jwt, ...) into one-call setups.\n\n  - jwt_builder_settyp() sets the \"typ\" header; jwt_checker_expect_typ() requires\n    it on verify, matching case-insensitively and tolerating the optional\n    \"application/\" prefix (RFC 6838) — the standardized cross-JWT-confusion\n    defense (RFC 8725 3.11).\n  - jwt_checker_setalgs() restricts the accepted algorithms to a set, checked\n    before any signature work (and so blocks an alg:none downgrade). Useful with\n    a keyring (jwt_checker_setkeyring) where several algs are acceptable.\n\nBoth enforced in the Compact path (__verify_config_post) and per signature in\nthe JSON path (verify_entry); a JSON signature failing the typ/alg policy is\nsimply not accepted, so the verification policy still decides.\n\ntests/jwt_typ_alg.c (both JSON backends, all crypto backends): typ accepted\nexact / case-folded / application/-prefixed and rejected on mismatch or when\nabsent; allowlist accepts a permitted alg, rejects a non-permitted one and an\nalg:none downgrade; clearing either restores acceptance. 31/31 suites pass\nlocally (MbedTLS 4.1) and in the CI image (MbedTLS 3.6.6); valgrind-clean.\n@since 3.6.0; README standards table updated (also adds the RFC 7797 row).\n\nCloses #312\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "da5c24cd5ce3a3ab1675a9ddf3bbcf54b7dcb420",
      "tree": "83e5456d0cc0bd7e5dfc50008d3ab3f91467e04d",
      "parents": [
        "825b0a2fe3899e4658d08e00f8faea5f6ece13f8",
        "eca511ece30bd7e49c865d07d14f4f772c11a599"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 07:50:43 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 07:50:43 2026 -0400"
      },
      "message": "Merge pull request #329 from benmcollins/315-rfc7797-b64\n\njwt: RFC 7797 unencoded (b64\u003dfalse) + detached payloads"
    },
    {
      "commit": "eca511ece30bd7e49c865d07d14f4f772c11a599",
      "tree": "83e5456d0cc0bd7e5dfc50008d3ab3f91467e04d",
      "parents": [
        "2469253c6c878a8ccc9b384cd24189ded0e0b750"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 07:33:15 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 07:33:15 2026 -0400"
      },
      "message": "jwt: RFC 7797 review fixes — reject NUL in unencoded payload; empty detached error\n\nFrom the adversarial review of #315:\n\n- An unencoded (b64\u003dfalse) payload appears verbatim in the serialized token (a\n  C string / JSON string), which cannot carry an embedded NUL — it would\n  silently truncate and the token would fail its own verification. Reject such\n  a payload at generate with a clear error instead.\n\n- jwt_checker_verify_detached() with an empty (len 0) payload under b64\u003dtrue\n  returned failure with no error message (detached_payload_part treated the\n  empty base64url as an error). Return an empty payload part so the normal\n  verify runs and reports a proper error; set an error on the (now OOM-only)\n  failure paths.\n\ntests/jws_b64.c: add the embedded-NUL rejection case.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "2469253c6c878a8ccc9b384cd24189ded0e0b750",
      "tree": "9c1f5e4cbf3ec4d23b762dfdf63bc431cf2cebf9",
      "parents": [
        "825b0a2fe3899e4658d08e00f8faea5f6ece13f8"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 07:19:51 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 07:19:51 2026 -0400"
      },
      "message": "jwt: add RFC 7797 unencoded (b64\u003dfalse) and detached payloads\n\nAdd the JWS RFC 7797 \"b64\":false option and detached payloads, for generic-JWS\nuse (HTTP message signatures, JAdES/eIDAS) — RFC 7797 forbids the option inside\nJWTs, so this signs an opaque payload, not JSON claims.\n\nAPI:\n  - jwt_builder_setpayload(): sign a raw, opaque payload instead of claims.\n  - jwt_builder_setb64(b, 0): emit \"b64\":false, mark \"b64\" critical, and sign\n    over the RAW payload bytes (RFC 7797 §3). Requires a raw payload.\n  - jwt_builder_set_detached(): omit the payload from the output (empty middle\n    in Compact; no \"payload\" member in the JSON forms).\n  - jwt_checker_verify_detached(): verify supplying the payload out-of-band;\n    reconstructs the attached token and runs the normal verify, so it works for\n    Compact and both JSON serializations and for b64 true/false.\n\nAcross all serializations (Compact, JSON Flattened, JSON General multi-sig):\nthe signing input is BASE64URL(protected) \".\" (base64url-or-raw) payload, built\nbinary-safely. Compact b64\u003dfalse uses last-dot splitting so a raw payload may\ncontain \u0027.\u0027. An unencoded payload is opaque, so claim checks are skipped.\n\nSecurity (RFC 7797 §6, load-bearing): the checker REJECTS a \"b64\":false token\nunless \"b64\" is present in \"crit\" (enforced before any signature work); \"b64\"\nis treated as library-understood for the crit check.\n\nAlso fixes a latent off-by-one exposed by binary-safe assembly: the encoder now\nuses the true base64url string length (jwt_base64uri_encode\u0027s return counts\nstripped \u0027\u003d\u0027 padding). Standard tokens are byte-identical.\n\ntests/jws_b64.c (both JSON backends, all crypto backends): b64\u003dfalse\nattached/detached across all serializations, b64\u003dtrue detached JWT, the\nRFC 7797 Appendix A.4 vector (exact bytes), and the crit-bypass rejection.\n30/30 suites pass locally (MbedTLS 4.1) and in the CI image (MbedTLS 3.6.6);\nvalgrind-clean.\n\nCloses #315\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "825b0a2fe3899e4658d08e00f8faea5f6ece13f8",
      "tree": "d9deea33b4eb7d45a2ce32bbde492925a40812cc",
      "parents": [
        "8a5584454cf1562973c2f2f4ea3fde4ab151be5b",
        "b6d2c1cfd3173093bfeda1b5413d1b9f3af718e6"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 06:20:49 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 06:20:49 2026 -0400"
      },
      "message": "Merge pull request #328 from benmcollins/327-xbackend-free\n\njwks: free a key\u0027s provider_data via its origin backend (#327)"
    },
    {
      "commit": "b6d2c1cfd3173093bfeda1b5413d1b9f3af718e6",
      "tree": "d9deea33b4eb7d45a2ce32bbde492925a40812cc",
      "parents": [
        "8a5584454cf1562973c2f2f4ea3fde4ab151be5b"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 06:03:07 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 06:03:07 2026 -0400"
      },
      "message": "jwks: free a key\u0027s provider_data via its origin backend (#327)\n\n__item_free() released a key\u0027s backend-specific provider_data with the ACTIVE\njwt_ops (jwt_ops-\u003eprocess_item_free), not the backend that parsed the key. A key\nparsed under one backend and freed while another is active had its EVP_PKEY /\nGnuTLS key / MbedTLS PSA handle freed by the wrong backend, which does not\nrecognize the handle: the provider_data leaked (and was undefined behavior).\nThis is the free-path gap left by #320, which routed only crypto operations.\n\nRoute the free through jwt_item_ops(item), exactly as #320 did for sign/verify\nand JWE. oct keys (JWT_CRYPTO_OPS_ANY) are still freed directly; an error key\n(provider NONE) resolves to the active ops as before.\n\ntests/jwt_xbackend.c: test_xbackend_free parses EC/RSA/oct under every backend\nand frees each while a different backend is active. Proven under CK_FORK\u003dno\nvalgrind in the CI image: 37,674 bytes definitely lost / 32 errors WITHOUT the\nfix, 0 lost / 0 errors WITH it.\n\nCloses #327\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "8a5584454cf1562973c2f2f4ea3fde4ab151be5b",
      "tree": "d85e86d704d58ec0d5758fcb11ed4fa37a6195fa",
      "parents": [
        "de0770ecc473d078d6b1ddc2010ad61df35c9d1a",
        "b3f1a7071fe0562a26eb0528a0472c907d558c96"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 05:57:54 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 05:57:54 2026 -0400"
      },
      "message": "Merge pull request #326 from benmcollins/308-jws-json\n\njwt: JWS JSON Serialization (multi-signature) + keyring verify"
    },
    {
      "commit": "b3f1a7071fe0562a26eb0528a0472c907d558c96",
      "tree": "d85e86d704d58ec0d5758fcb11ed4fa37a6195fa",
      "parents": [
        "eafe5e2e6e7a2505d0c7967222f83dda89f961ee"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 05:39:35 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 05:39:35 2026 -0400"
      },
      "message": "jwt: fix JWS JSON verify header leak; parse test keys under active backend\n\nTwo fixes from an adversarial review + valgrind memcheck of #308:\n\n- jwt_verify_json() created the transient JWT with jwt_new(), which allocates an\n  owned (empty) jwt-\u003eheaders; the function released jwt-\u003eclaims before\n  reassigning it but not jwt-\u003eheaders. Each per-signature step then overwrote\n  jwt-\u003eheaders with a *borrowed* protected header, orphaning the original — one\n  JSON object leaked per JSON verification. Release it right after jwt_new(),\n  since the function only ever borrows a signature\u0027s protected header thereafter.\n\n- tests/jws_json.c: three tests parsed their key (load_one) in the declaration\n  initializer, before SET_OPS(), so the key was parsed under one backend and\n  freed under another, leaking the backend-specific provider_data. Parse under\n  the active backend instead, matching the other tests.\n\nVerified leak-free under CK_FORK\u003dno valgrind in the CI image (MbedTLS 3.6.6):\n0 bytes lost, 0 errors, 24 checks.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "eafe5e2e6e7a2505d0c7967222f83dda89f961ee",
      "tree": "b8c88b6d47467f9fd2a88fe791712776ed246624",
      "parents": [
        "de0770ecc473d078d6b1ddc2010ad61df35c9d1a"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 05:23:05 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 05:23:05 2026 -0400"
      },
      "message": "jwt: add JWS JSON Serialization (multi-signature) + keyring verify\n\nImplement RFC 7515 §7.2, the JWS JSON Serialization, in both the Flattened\n(single signature) and General (a \"signatures\" array of one or more) forms,\nalongside the existing Compact Serialization. This is the structural\nprerequisite for multi-signature tokens (issuer rotation, multi-issuer\nco-signing) and per-signature headers.\n\nBuilder:\n  - jwt_serialization_t {COMPACT, JSON_FLAT, JSON_GENERAL} + jwt_builder_set_format()\n  - jwt_builder_add_signature() -\u003e jwt_signature_t for additional signers (a\n    second signer auto-promotes to General); each signs over its OWN protected\n    header and the shared payload, so signers may mix algorithms (RS256+ES256)\n  - jwt_signature_add_protected_json() / _add_header_json() for per-signature\n    protected and unprotected headers\n  - \"none\" is refused in any JSON-serialized JWS (RFC 8725)\n\nChecker (multi-key, explicit policy):\n  - jwt_checker_setkeyring(checker, jwk_set_t, jwt_verify_policy_t): verify a\n    multi-signature token against a JWKS. ANY (default, accept if \u003e\u003d1 signature\n    verifies) or ALL (every signature must). A \"kid\"-named signature is matched\n    to that key; a keyless one is tried against every compatible key, always\n    under the jwt_alg_required_kty anti-confusion gate (GHSA-q843-6q5f-w55g),\n    routed to each key\u0027s origin backend via jwt_item_ops()\n  - jwt_checker_verify() auto-detects \u0027{\u0027 (JSON) vs Compact, unchanged signature\n  - jwt_checker_sig_count()/_sig_verified()/_sig_key() report per-signature results\n  - read-only claim checks run once on the shared payload; the jti replay\n    callback runs once, only after the signature policy is satisfied\n\nThe signing input reuses the verbatim base64url protected header and payload\nbytes (never re-serialized). The Compact path is byte-identical to before.\n\nCLI: jwt-generate gains --format and repeated --key (multi-signer); jwt-verify\ngains --keyring and --policy. Man pages + README updated.\n\nTests (tests/jws_json.c, both JSON backends, all crypto backends, CI image):\nround-trip Flattened/General, ANY vs ALL policy, kid matching, tampering,\nmalformed/type-confusion guards, per-signature unprotected header, header\noverlap, and the introspection/error paths. BATS covers the new CLI flags.\n\nCloses #308\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "de0770ecc473d078d6b1ddc2010ad61df35c9d1a",
      "tree": "8929f5f8d0a2c745224dfc18bae0817715167f99",
      "parents": [
        "86cedec34cdfd10068ed5608b367985902259158",
        "f4a536fe70aa37e454e4b2ead3b01281f1fe0f40"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 04:08:12 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 04:08:12 2026 -0400"
      },
      "message": "Merge pull request #325 from benmcollins/320-backend-affinity\n\njwt: tie key operations to the backend that parsed the key"
    },
    {
      "commit": "f4a536fe70aa37e454e4b2ead3b01281f1fe0f40",
      "tree": "8929f5f8d0a2c745224dfc18bae0817715167f99",
      "parents": [
        "86cedec34cdfd10068ed5608b367985902259158"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 03:51:15 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 03:51:15 2026 -0400"
      },
      "message": "jwt: route key operations to the backend that parsed the key\n\nA jwk_item is bound to the crypto backend active when its JWK was parsed\n(item-\u003eprovider). Previously, signing/verifying or JWE-encrypting with that\nkey under a DIFFERENT active backend failed (\"Key is not compatible\" on\nOpenSSL/MbedTLS; GnuTLS silently re-imported from the JWK JSON).\n\nAdd jwt_item_ops(), which resolves a key\u0027s origin backend from the ops\nregistry, and dispatch the asymmetric key operations through it: sign/verify\n(jwt.c) and the JWE ECDH-ES and RSA key-management paths (jwe.c). Keys with no\nbackend-specific material (\"oct\" / JWT_CRYPTO_OPS_ANY) and key-free operations\n(content encryption on a raw CEK, the CSPRNG, the RFC 7638 thumbprint digest,\nJWK JSON export) continue to use the active backend.\n\nMixed-backend usage now \"just works\", OpenSSL/GnuTLS/MbedTLS behave\nconsistently, and the \"set crypto ops before loading keys\" ordering\nrequirement (the root cause behind #321) is no longer needed.\n\ntests/jwt_xbackend.c exercises the full (parse-backend, use-backend) matrix:\nfor each pair a key parsed under one backend signs+verifies (EC ES256, RSA\nRS256, oct HS256) and round-trips JWE (RSA-OAEP-256, ECDH-ES) under another.\nVerified across all three backends under both JSON backends locally\n(MbedTLS 4.1) and in the CI image (MbedTLS 3.6.6).\n\nCloses #320\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "86cedec34cdfd10068ed5608b367985902259158",
      "tree": "ce2c7c93e21a3e33862a47c4fdc2e2c5cc82f068",
      "parents": [
        "b1f0dd52b4b6a21c5e996b40065b3639f9dc0a3b",
        "88d388592ef85e50c8b07d7177cfabb32937a73f"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 03:37:47 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 03:37:47 2026 -0400"
      },
      "message": "Merge pull request #324 from benmcollins/docs-mbedtls-4x\n\ndocs: MbedTLS 4.x builds (drop stale \"4.x will not build\" note)"
    },
    {
      "commit": "88d388592ef85e50c8b07d7177cfabb32937a73f",
      "tree": "ce2c7c93e21a3e33862a47c4fdc2e2c5cc82f068",
      "parents": [
        "b1f0dd52b4b6a21c5e996b40065b3639f9dc0a3b"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 03:26:51 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 03:26:51 2026 -0400"
      },
      "message": "docs: MbedTLS 4.x builds (PSA backend); drop stale \"4.x will not build\"\n\nThe MbedTLS backend uses PSA Crypto (psa/crypto.h), present in both MbedTLS\n3.6.x LTS and 4.x, so libjwt builds against either (verified building +\npassing the suite against 4.1.0; mbedtls/jwk-export.c already has\nMBEDTLS_VERSION_MAJOR \u003e\u003d 4 paths).\n\nCorrect the stale \"4.x will not build / drops the mbedtls_pk_* API\" wording in\nCLAUDE.md, the CI-image Dockerfile header comment, and the docker README. The\nCI image still pins 3.6.6 as the tested LTS baseline (a deliberate choice, not\na limitation).\n\nCloses #323\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "b1f0dd52b4b6a21c5e996b40065b3639f9dc0a3b",
      "tree": "72c69eb326aff173f0422a7f8dfe81e2d5a78bbd",
      "parents": [
        "a486608729014180de07a153d21a4039329a1697",
        "24281596851cdb7302864322fc6397c4967bc9a8"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 03:22:31 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 03:22:31 2026 -0400"
      },
      "message": "Merge pull request #322 from benmcollins/321-cnf-load-order\n\ntests: jwt_cnf: set crypto ops before loading keys"
    },
    {
      "commit": "24281596851cdb7302864322fc6397c4967bc9a8",
      "tree": "72c69eb326aff173f0422a7f8dfe81e2d5a78bbd",
      "parents": [
        "a486608729014180de07a153d21a4039329a1697"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 03:07:01 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 03:07:01 2026 -0400"
      },
      "message": "tests: jwt_cnf: set crypto ops before loading keys\n\nA jwk_item is bound to the crypto backend active when the JWK is parsed, so a\nkey must be loaded under the backend it will be used with. jwt_cnf loaded its\nkey BEFORE SET_OPS(), binding it to the default backend (OpenSSL), then signed\nunder the loop\u0027s backend -- which failed on MbedTLS with \"Key is not\ncompatible\". PR #319 masked this by signing with an oct (backend-agnostic) key.\n\nMove SET_OPS() ahead of every key load (matching jwt_flipflop.c) and restore\nthe simpler, representative single EC key / ES256 design (the key both signs\nthe token and is bound via cnf).\n\nVerified across openssl/gnutls/mbedtls under both JSON backends locally\n(MbedTLS 4.1) and in the CI image (MbedTLS 3.6.6), where the original ordering\nreproduced the failure.\n\nCloses #321\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "a486608729014180de07a153d21a4039329a1697",
      "tree": "946ba3145eeb29d12f4d9f1e29e2fdb141d5a10b",
      "parents": [
        "a05da90eb3c5c3c02b91c7cfb1f81a6b862b9335",
        "a69d82358a143d9957e587dc94e666fe66a5a739"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 02:23:44 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 02:23:44 2026 -0400"
      },
      "message": "Merge pull request #319 from benmcollins/316-cnf\n\njwt: RFC 7800 cnf (confirmation) claim helpers"
    },
    {
      "commit": "a69d82358a143d9957e587dc94e666fe66a5a739",
      "tree": "946ba3145eeb29d12f4d9f1e29e2fdb141d5a10b",
      "parents": [
        "a05da90eb3c5c3c02b91c7cfb1f81a6b862b9335"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 01:53:42 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 02:03:39 2026 -0400"
      },
      "message": "jwt: add RFC 7800 \"cnf\" (confirmation) claim helpers\n\nAdd convenience helpers for the proof-of-possession \"cnf\" claim, the\nsubstrate for DPoP (RFC 9449), OAuth mTLS (RFC 8705), and OpenID PoP:\n\n  - jwt_builder_setcnf_jkt()  cnf \u003d {\"jkt\": RFC 7638 thumbprint of key}\n  - jwt_builder_setcnf_jwk()  cnf \u003d {\"jwk\": public JWK of key}\n  - jwt_builder_setcnf()      cnf \u003d {member: value} (kid, x5t#S256, jku, ...)\n  - jwt_get_cnf()             read a string cnf member from a verified jwt_t\n\nThe setters reuse the #307 thumbprint primitive and the existing typed claim\nAPI; each REPLACES any prior \"cnf\", so the object always holds the single\nconfirmation member RFC 7800 expects. jkt is the DPoP method; computing\nx5t#S256 from a DER cert is the X.509 work in #314 (the value can be set\ndirectly today via setcnf()).\n\nA verifier reads cnf from the jwt_t handed to its jwt_checker_setcb()\ncallback (the checker exposes only registered claims directly), so the\ngetter is a jwt_t accessor rather than the jwt_checker_get_cnf_jkt() the\nissue sketched.\n\nTested under both JSON backends across the crypto-ops loop: jkt/jwk/generic\nset round-trips, single-member enforcement, callback-based get, and the\nabsent / non-string / seed-only-OKP error paths.\n\nCloses #316\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "a05da90eb3c5c3c02b91c7cfb1f81a6b862b9335",
      "tree": "26100f7f9b64eda9faa5537551fd4022f9aebe5a",
      "parents": [
        "3c7bd2a969289fcc116513f2579228c675c48154",
        "590beb27d3b09615f8d3d44e4a57d093f0ca314e"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Thu Jun 18 01:25:04 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 01:25:04 2026 -0400"
      },
      "message": "Merge pull request #318 from benmcollins/307-jwk-thumbprint\n\njwks: RFC 7638 JWK Thumbprint and RFC 9278 Thumbprint URI"
    },
    {
      "commit": "590beb27d3b09615f8d3d44e4a57d093f0ca314e",
      "tree": "26100f7f9b64eda9faa5537551fd4022f9aebe5a",
      "parents": [
        "690345050d81a5230e87920dd7cf5c9c0f89fcc7"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 01:08:02 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 01:08:02 2026 -0400"
      },
      "message": "openssl: export public \"x\" for private OKP keys (RFC 8037)\n\nThe OpenSSL backend exported only \"d\" for a private OKP (Ed25519/Ed448/\nX25519/X448) key -- unlike the GnuTLS backend, which emits both, and contrary\nto RFC 8037, where a private OKP JWK carries both \"x\" and \"d\". Always export\n\"x\", adding \"d\" only for a private key.\n\nThis makes private OKP JWKs standards-compliant and identical across\nbackends, lets every OKP key get a thumbprint/\"kid\", and keeps a re-imported\nJWK off the seed-only path that crashes GnuTLS \u003c 3.8.13.\n\nUpdates the key2jwk golden output (tests/cli/all.json) accordingly.\n\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "690345050d81a5230e87920dd7cf5c9c0f89fcc7",
      "tree": "05785cb3a4f0df2a561ede2067bbebaad59d2f9d",
      "parents": [
        "f648626538150d58b6fb30b825190392228a123a"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 01:08:02 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 01:08:02 2026 -0400"
      },
      "message": "tools: generate a deterministic thumbprint \"kid\" and disambiguate jwk2key\n\nkey2jwk\u0027s JWK_KEY_GEN_KID now sets \"kid\" to the RFC 7638 SHA-256 JWK\nthumbprint instead of a random uuidv4, so the same key always gets the same\ncanonical \"kid\". The thumbprint core is factored into an internal helper\n(jwt_jwk_thumbprint) shared by jwks_item_thumbprint() and the kid generator.\n\njwk2key now names output files by a stable per-key id -- the \"kid\" if\npresent, otherwise the thumbprint -- so multiple keyless keys of the same\ntype no longer collide on a single file name.\n\nUpdates the man pages, the gen-kid unit test, and the CLI tests (keyless oct\noutput is now named by thumbprint).\n\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "f648626538150d58b6fb30b825190392228a123a",
      "tree": "72b71e4d60ac9c57a2fac69945a02b489d510cf8",
      "parents": [
        "3c7bd2a969289fcc116513f2579228c675c48154"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 00:26:02 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Thu Jun 18 01:07:44 2026 -0400"
      },
      "message": "jwks: add RFC 7638 JWK Thumbprint and RFC 9278 Thumbprint URI\n\nAdd public APIs to compute and match a key\u0027s deterministic fingerprint:\n\n  - jwks_item_thumbprint()      RFC 7638 base64url(SHA-2(canonical JWK))\n  - jwks_item_thumbprint_uri()  RFC 9278 urn:ietf:params:oauth: form\n  - jwks_find_bythumbprint()    find a key in a set by its thumbprint\n  - jwks_find_bythumbprint_uri()\n\nThe hash is selected with a new jwk_thumbprint_alg_t enum (SHA-256 the\ndefault, plus SHA-384/512). Finding by thumbprint matches a key on its\ncanonical, deterministic identity rather than the advisory \"kid\", which is\nuseful for proof-of-possession (cnf/jkt) lookups.\n\nThe canonical JWK is built from only the required members for each key type\nand serialized sorted + compact, reusing the same canonicalization the\nlibrary already uses for signing, so the digest is identical across the\nJansson and json-c backends and member values are escaped by that trusted\npath rather than by hand. A new crypto-ops \"sha\" one-shot digest primitive\nbacks this in all three backends (OpenSSL, GnuTLS, MbedTLS).\n\nVerified against the RFC 7638 Section 3.1 test vector and per-key-type\nfixtures (EC, RSA, oct, OKP, AKP) under both JSON backends; a private key\nand its public counterpart yield the same thumbprint, and a seed-only OKP\nkey (no public \"x\") fails cleanly.\n\nCloses #307\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "3c7bd2a969289fcc116513f2579228c675c48154",
      "tree": "3654d6cc5a917344e5c6339e1c6ef058e84df10d",
      "parents": [
        "8f32b58ad0e9d6e03f2fbf0f863f713506eabf5f"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Wed Jun 17 21:45:30 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Wed Jun 17 21:45:30 2026 -0400"
      },
      "message": "Add Ed448 to EdDSA alg type comment.\n\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "8f32b58ad0e9d6e03f2fbf0f863f713506eabf5f",
      "tree": "55507a8e133936e4d2f809a9c3da501cc41d4ec7",
      "parents": [
        "077941a211fbf6df0aa14486b4c8160600a812de"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Wed Jun 17 20:48:54 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Wed Jun 17 20:48:54 2026 -0400"
      },
      "message": "docs: document the *_auto_t cleanup macros (and let Doxygen render them)\n\nThe jwt_builder_auto_t / jwt_checker_auto_t / jwe_builder_auto_t /\njwe_checker_auto_t / jwk_set_auto_t scope-cleanup macros had no doc\nblock of their own. Give each a @brief + @since (3.0.0 for the JWT and\nJWKS macros, 3.4.0 for the JWE ones).\n\nThese macros and their matching *_freep helpers sit behind\n`#if defined(__GNUC__) || defined(__clang__)`. Doxygen predefines\nneither, so it evaluated the guard as false and stripped the whole\nblock -- including the *_freep @since added in the previous pass -- from\nthe generated docs. Add `|| defined(_DOXYGEN)` to those five guards so\nDoxygen (which predefines _DOXYGEN in DOXYGEN_PREDEFINED) documents\nthem. This is inert in a real build, where _DOXYGEN is never defined.\n\nWhile those *_freep blocks are now rendered, fix their stale\nidentifiers: jwt_set_auto_t -\u003e jwk_set_auto_t, jwt_set_t -\u003e jwk_set_t,\nand \"jwt checker...\" -\u003e \"jwt_checker...\".\n\nDoc-only; the library build is unchanged and Doxygen builds clean.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "077941a211fbf6df0aa14486b4c8160600a812de",
      "tree": "10cf3646f8a7d2089d61eb918bb595770682085b",
      "parents": [
        "8e017b59a4023f9c00bd069a2e78f2484da73efb"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Wed Jun 17 17:31:51 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Wed Jun 17 17:31:51 2026 -0400"
      },
      "message": "docs: track @since maintenance in the release process\n\nNote in the Version + SONAME review step that the same exported-symbol,\nenum, and macro diff must also drive the Doxygen @since tags in\ninclude/jwt.h, so any API new at a release is annotated with the version\nthat introduced it. Calls out the asymmetry: new enum values and\n#cmakedefine macros are not linker symbols (no SONAME-triple change) but\nstill need @since.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "8e017b59a4023f9c00bd069a2e78f2484da73efb",
      "tree": "ed645476c789d2d8d26314df0d9c3c272dcae481",
      "parents": [
        "9cab0216fa6808a391a90b1a2e2e5b78539aad99",
        "3f588f396515fa77ad4808fde01bfbe7bc74da28"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Wed Jun 17 17:31:09 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Jun 17 17:31:09 2026 -0400"
      },
      "message": "Merge pull request #306 from benmcollins/305-since-annotations\n\ndocs: annotate public API with @since version in jwt.h"
    },
    {
      "commit": "3f588f396515fa77ad4808fde01bfbe7bc74da28",
      "tree": "ed645476c789d2d8d26314df0d9c3c272dcae481",
      "parents": [
        "9cab0216fa6808a391a90b1a2e2e5b78539aad99"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Wed Jun 17 17:16:27 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Wed Jun 17 17:16:27 2026 -0400"
      },
      "message": "docs: annotate public API with @since version in jwt.h\n\nAdd @since X.Y.Z to every documented public symbol in the public\nheader, recording the LibJWT version that first introduced it. The\nAPI has been purely additive since the v3.0.0 reset (nothing removed\nor changed), so every symbol present at v3.0.0 is @since 3.0.0 and\nlater additions carry their introducing tag.\n\nVersions were derived by diffing include/jwt.h across every 3.x tag\n(v3.0.0..v3.5.0, patch releases included) and cross-checked against\nthe git tag boundaries (present at its tag, absent at the prior tag):\n\n  - 117 JWT_EXPORT functions (74 @3.0.0, 2 @3.1.0, 3 @3.2.0, 38 @3.4.0)\n  - 27 typedef/enum/struct blocks (18 @3.0.0, 9 @3.4.0)\n  - 5 static inline *_freep helpers (3.0.0; jwe @3.4.0)\n  - 8 jwt_set_GET/SET_* macros (@3.0.0)\n  - 4 enum members added after their enum type: JWT_ALG_ML_DSA_44/65/87\n    and JWK_KEY_TYPE_AKP (@3.5.0, inline)\n  - jwt_long_t: first introduced in v3.2.2; add a minimal doc block so\n    it can carry @since 3.2.2\n\nDoc-only; no code or ABI change. Doxygen builds clean with no new\nwarnings.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "9cab0216fa6808a391a90b1a2e2e5b78539aad99",
      "tree": "d0f6dd731ffc62a9b51187795dc15159bd7f1890",
      "parents": [
        "08ef4cee227d1f417796bcde465777426afd386d"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 15:28:42 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 15:28:42 2026 -0400"
      },
      "message": "docs: document the release process in CLAUDE.md\n\nAdd a \"Release Process\" section covering the version/SONAME bump in\nLibJWTVersions.cmake (libtool triple is symbol-counted via nm, not\nfeature-counted), the release-X.Y.Z branch/commit/PR/merge flow, the\nannotated tag on the merge commit, the manually-built source tarball\n(tracked files minus .github/ and .gitignore, not CPack), and the\nGitHub Release with its tarball asset.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "08ef4cee227d1f417796bcde465777426afd386d",
      "tree": "c1062d745318a17a08b51bfa213b452c0e2f518d",
      "parents": [
        "60384cbc2b69acb5f8dfd8000c81841c0fb4f645",
        "502eac06cbbb61dbb72290a038bf34bfc0e7c238"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Tue Jun 16 13:47:59 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 16 13:47:59 2026 -0400"
      },
      "message": "Merge pull request #304 from benmcollins/release-3.5.0\n\nRelease v3.5.0"
    },
    {
      "commit": "502eac06cbbb61dbb72290a038bf34bfc0e7c238",
      "tree": "c1062d745318a17a08b51bfa213b452c0e2f518d",
      "parents": [
        "60384cbc2b69acb5f8dfd8000c81841c0fb4f645"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 12:47:09 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 12:47:09 2026 -0400"
      },
      "message": "Release v3.5.0\n\nBump version 3.4.0 -\u003e 3.5.0 and the SONAME triple (libtool c:r:a)\n17:0:3 -\u003e 17:1:3. Per the libtool rules\n(http://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html):\nthe source changed, but no interfaces (exported symbols) were added,\nremoved, or changed this cycle (nm: 118 -\u003e 118 public symbols), so only\nrevision is incremented -- current and age are untouched. The SONAME\nstays libjwt.so.14 and existing binaries remain compatible (the new\nlibrary is a drop-in for the old, and a program built against 3.5.0\nstill resolves cleanly against the 3.4.0 library: libtool case 1).\n\nThe experimental ML-DSA support added this cycle is exposed through new\njwt_alg_t / jwk_key_type_t enumerators (JWT_ALG_ML_DSA_44/65/87,\nJWK_KEY_TYPE_AKP) and the LIBJWT_HAVE_ML_DSA feature macro, not new\nfunctions, so it contributes no linker symbols and does not move\ncurrent/age.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "60384cbc2b69acb5f8dfd8000c81841c0fb4f645",
      "tree": "b74e859401e91ab8fc0549471ab562ec01c3b6c1",
      "parents": [
        "4860069d2f470ce91ca811ae2eaecc563ad8f9e5"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 12:20:22 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 12:20:22 2026 -0400"
      },
      "message": "doxygen: read sources as UTF-8 (was ISO-8859-1)\n\nThe doc sources (include/jwt.h doc comments, doxygen/mainpage.dox) are UTF-8, but\nDOXYGEN_INPUT_ENCODING was ISO-8859-1, so doxygen decoded multi-byte UTF-8 (em\ndashes, curly quotes, symbols) as Latin-1 and emitted mojibake in the generated\nHTML. Set it to UTF-8 so non-ASCII renders correctly; update the CLAUDE.md note\naccordingly (don\u0027t revert it).\n\nVerified with a doxygen 1.15.0 A/B run on a UTF-8 em-dash: UTF-8 input yields the\ncorrect U+2014 (e2 80 94) in the HTML, ISO-8859-1 yields the c3 a2 c2 80 c2 94\nmojibake.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "4860069d2f470ce91ca811ae2eaecc563ad8f9e5",
      "tree": "2609027ff87378a5bc8764b71aaa3777cc0bb313",
      "parents": [
        "9a16027299d4f167be11fe1b4ecb3e8353f5bdf9"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 12:03:26 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 12:03:26 2026 -0400"
      },
      "message": "docs: drop non-ASCII (em-dashes) from doxygen-rendered docs\n\nDoxygen reads input as ISO-8859-1 (DOXYGEN_INPUT_ENCODING in\ncmake/LibJWTDoxyfile.cmake) while the sources are UTF-8, so a UTF-8 em-dash in\ndoxygen/mainpage.dox (or include/jwt.h) renders as mojibake in the generated\nHTML. Replace the em-dash in the GnuTLS OKP @note with ASCII, and ASCII-clean\nthe markdown READMEs (em-dashes, plus the arrows/\u003e\u003d in .github/docker/README.md)\nfor consistency. Add a CLAUDE.md note recording the cause and the ASCII-only\nrule for doxygen-rendered docs.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "9a16027299d4f167be11fe1b4ecb3e8353f5bdf9",
      "tree": "d680b7b24cab3e1de6fbd1f42fc9729d58282175",
      "parents": [
        "949d50f3b867d6ca48e29bff50a7f02bb33bf0f0"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 11:51:31 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 11:51:31 2026 -0400"
      },
      "message": "docs: record the CI image, matrix rework, and GnuTLS OKP gating in CLAUDE.md\n\nUpdate CLAUDE.md for this session\u0027s changes:\n\n  - Continuous Integration: rewrite for the GHCR base image\n    (gnutls-leancrypto-mbedtls, built/pushed by ci-image.yml), the\n    build-linux-combos \u0027all\u0027 row that carries coverage + memcheck + Codecov,\n    the build-linux vendor-compat matrix (both JSON backends, OpenSSL-only,\n    GnuTLS off), the json-c 0.16 floor / jammy exclusion, and the removal of\n    build-linux-mbedtls and build-linux-json-c. Clarify that the in-CI\n    check-code-coverage step is informational (runs under `|| true`); the\n    no-new-uncovered-lines rule is a local discipline, not a CI gate.\n  - Version-gated features: document the hybrid dynamic-gating pattern\n    (build-time `#if` + runtime gnutls_check_version) and the two GnuTLS \u003c 3.8.13\n    OKP defects it guards (seed-only import_ecc_raw segfault; X25519/X448).\n  - Note the MbedTLS 3.6.x-only ceiling (4.x drops the legacy mbedtls_pk_* API).\n  - Testing: add gnutls_okp_jwk_broken() as a second runtime-probe example.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "949d50f3b867d6ca48e29bff50a7f02bb33bf0f0",
      "tree": "d0f218f493ed434703f5301d380390301c1b2378",
      "parents": [
        "1d839c2a47e821a4bcbab114a7b6eccaa52ec6d5"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 11:50:25 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 11:50:25 2026 -0400"
      },
      "message": "Update social preview images\n\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "1d839c2a47e821a4bcbab114a7b6eccaa52ec6d5",
      "tree": "74edd4147397280bddaa38ad28e6b51154de74b6",
      "parents": [
        "4995771d55e775cd46eb563a9ea10fb43e73f0dc"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 11:23:48 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 11:23:48 2026 -0400"
      },
      "message": "gnutls: gate the two OKP defects in GnuTLS \u003c 3.8.13 (runtime-checked)\n\nGnuTLS \u003c 3.8.13 has two defects in the native OKP path that libjwt triggers:\n\n  1. gnutls_privkey_import_ecc_raw() SEGFAULTS when it has to derive the public\n     key for a \"seed-only\" OKP private JWK - one with \u0027d\u0027 but no public\n     coordinate \u0027x\u0027 (any curve: Ed25519, Ed448, X25519, X448).\n  2. X25519/X448 ECDH-ES key agreement does not work (derive yields nothing),\n     even when the key carries \u0027x\u0027.\n\nA library must not crash on a key it cannot handle, so gnutls_process_eddsa()\nnow rejects these cases with a clear error instead. Keys that carry \u0027x\u0027, all\npublic keys, and the PEM/DER path (whose JWK export always includes \u0027x\u0027) are\nunaffected, as are the OpenSSL and MbedTLS backends.\n\nThe gate is hybrid so a shared-library upgrade lifts it without rebuilding\nlibjwt: a compile-time `#if GNUTLS_VERSION_NUMBER \u003c 0x03080d` keeps the code\nabsent (and coverage-invisible) when built against \u003e\u003d 3.8.13, while the runtime\ngnutls_check_version(\"3.8.13\") decides at run time - so upgrading the system\nlibgnutls to \u003e\u003d 3.8.13 re-enables these keys with no libjwt rebuild.\n\nTests gain a gnutls_okp_jwk_broken() helper with the same hybrid build/runtime\ncheck, used to skip the affected keys on an older runtime GnuTLS (the keyring\nfree-bad accounting is adjusted for the two seed-only OKP keys it now rejects).\nThe supported-algorithm matrix in README.md and doxygen/mainpage.dox documents\nthe GnuTLS \u003e\u003d 3.8.13 requirement for these cases.\n\nVerified: full suite 100% on GnuTLS 3.8.9 (gate active) and 3.8.13 (gate absent,\nall backends + ML-DSA). The gate is #if\u0027d out on the 3.8.13 coverage build, so\nit adds no uncovered library lines.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "4995771d55e775cd46eb563a9ea10fb43e73f0dc",
      "tree": "f321c5c7c669819d5d30cd04c118194c82699a94",
      "parents": [
        "f8bc888086db32a86685b3435a46c983739a2c5f"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 10:09:32 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 10:09:32 2026 -0400"
      },
      "message": "ci: exclude json-c on ubuntu-22.04 (jammy ships json-c 0.15 \u003c 0.16 floor)\n\nThe expanded compat matrix builds every distro with both JSON backends, but\nUbuntu 22.04 ships json-c 0.15 and libjwt requires json-c \u003e\u003d 0.16, so that one\ncombination fails to configure. Exclude it; jammy still builds with jansson.\nAll other distros\u0027 json-c (noble 0.17, bookworm 0.16, trixie/resolute 0.18)\nclear the floor. 9 compat jobs.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "f8bc888086db32a86685b3435a46c983739a2c5f",
      "tree": "a57d34e9d91e975f12b3194c9df9098f776cf161",
      "parents": [
        "82a86ec1423b036912aa97a930e19d81a4136a5e"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 10:07:38 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 10:07:38 2026 -0400"
      },
      "message": "ci: clarify GnuTLS Ed448/X25519 breakage is the \u003c\u003d3.8.12-\u003e3.8.13 upgrade\n\nPinned via a debian:forky test (apt GnuTLS 3.8.13 + nettle 3.10.2 + p11-kit\npasses the Ed448/X25519/X448 tests): since trixie 3.8.9 has p11-kit too and\nstill fails, p11-kit is ruled out - it is the GnuTLS version upgrade. Update\nthe build-linux comment accordingly.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "82a86ec1423b036912aa97a930e19d81a4136a5e",
      "tree": "e00100589ae784009c07a87a22a6cbd971c31fbc",
      "parents": [
        "8ed636bbee5ed8f93b5cb1e9455ffe97fe382161"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 09:58:17 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 09:58:17 2026 -0400"
      },
      "message": "ci: build both JSON backends per distro; disable GnuTLS in compat matrix\n\nExpand the build-linux vendor-compat matrix to build each distro twice - once\nwith jansson, once with json-c - so both JSON backends are exercised across\ntheir shipped versions (5 distros x 2 \u003d 10 jobs).\n\nTurn GnuTLS OFF on every compat row. Enabling GnuTLS against the stacks these\ndistros ship (Debian trixie 3.8.9, Ubuntu resolute 3.8.12) revealed that libjwt\nSEGFAULTS loading Ed448 keys (jwt_flipflop, jwt_jwks) and returns NULL for\nX25519/X448 ECDH-ES (jwe_ecdh, jwe_json); the NIST P-curves are fine. Only the\nfrom-source GnuTLS 3.8.13 stack in the build-linux-combos image is known-correct\n(those rows pass). GnuTLS NEWS for 3.8.10-3.8.13 lists no Ed448/X448 fix, so the\ndifferentiator (GnuTLS vs nettle version vs the --without-p11-kit build) is not\nyet pinned down. Native GnuTLS coverage stays in build-linux-combos; re-enable\nGnuTLS here per distro once a stack is verified working.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "8ed636bbee5ed8f93b5cb1e9455ffe97fe382161",
      "tree": "4bab428ed7e2b03d9c33b924af2726f9a0319994",
      "parents": [
        "8d2391d7c738b7a5bca16f869a7e732360d143d0"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 09:13:19 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 09:13:19 2026 -0400"
      },
      "message": "ci: run backend combos in the leancrypto image; consolidate coverage\n\nPoint build-linux-combos at the new ghcr.io GnuTLS+leancrypto/MbedTLS base\nimage and make the all-backends row the comprehensive one:\n\n  - Rename the OpenSSL+GnuTLS+MbedTLS row to \"all\", add WITH_ML_DSA\u003dON, and\n    make it the single row that collects coverage (-\u003e Codecov) and runs\n    memcheck. On this image GnuTLS is built --with-leancrypto, so the ML-DSA\n    success path runs on both OpenSSL and GnuTLS (and is rejected by MbedTLS),\n    so those lines are exercised, not just compiled.\n  - Drop the separate ML-DSA matrix row (the \"all\" row supersets it).\n  - Remove build-linux-mbedtls (its coverage role moves to the \"all\" row) and\n    build-linux-json-c (folded into the compat matrix below).\n\nRepurpose build-linux into a vendor-compatibility matrix (ubuntu 22.04 / 24.04 /\n26.04, debian stable / oldstable). OpenSSL is enabled on every row; GnuTLS is\nenabled only where the distro ships gnutls \u003e\u003d 3.8.8 (trixie, resolute) and the\nolder distros build OpenSSL-only. The JSON backend alternates per row so json-c\nis exercised across its 0.16 / 0.17 / 0.18 vendor versions. ubuntu-26.04 is a\npreview runner, so that row is continue-on-error.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "8d2391d7c738b7a5bca16f869a7e732360d143d0",
      "tree": "bd773ab984d9e09d68e20ca1ea66105b7f0f3f17",
      "parents": [
        "47c7efedd052cf5d159f1bc3ab4488f4cb3dd18c"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 09:12:59 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 09:12:59 2026 -0400"
      },
      "message": "ci: add debian:forky GnuTLS+leancrypto / MbedTLS base image\n\nAdd a CI base image (.github/docker/Dockerfile) built on debian:forky with\nevery LibJWT backend usable in one place. Only the three components that need\na custom/newer build are compiled from source into /usr/local - leancrypto\nv1.7.2, GnuTLS 3.8.13 built --with-leancrypto (so the ML-DSA / FIPS 204 /\nRFC 9964 success path works; stock GnuTLS returns -106), and MbedTLS 3.6.6\n(3.6.x LTS, not the 4.x PSA rewrite). Everything else, and all build-deps,\ncome from native Debian apt. forky\u0027s libgnutls28-dev / libmbedtls-dev are\ndeliberately not installed so their .pc files cannot shadow the from-source\nones. The final layer asserts the pkg-config floors, /usr/local precedence,\nand a live certtool ML-DSA key-gen proving the leancrypto provider is wired in.\n\nPublish to ghcr.io/benmcollins/libjwt/gnutls-leancrypto-mbedtls via\n.github/workflows/ci-image.yml (build-push to GHCR with the built-in\nGITHUB_TOKEN, on workflow_dispatch and on Dockerfile changes).\n\nValidated locally: image builds, all asserts pass, and libjwt builds with all\nbackends + WITH_ML_DSA\u003dON with the full suite green (ML-DSA enabled via both\nOpenSSL 3.6.3 and GnuTLS 3.8.13).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "47c7efedd052cf5d159f1bc3ab4488f4cb3dd18c",
      "tree": "ee8dee8f58fb82d43aaf740890ef7a5d9b31b50f",
      "parents": [
        "afe663c065d7a6b3d3c9deac30e9cb0e624b032c"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 08:21:53 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 08:21:53 2026 -0400"
      },
      "message": "docs: record ML-DSA, CI, and coverage notes in CLAUDE.md\n\nCapture lessons from the ML-DSA work: the WITH_ML_DSA experimental-feature\npattern (LIBJWT_HAVE_ML_DSA via jwt_export.h.in, ABI-stable enums, per-backend\nversion guards, AKP/seed contract, GnuTLS leancrypto + seedless-export gotchas),\na Continuous Integration section (Codecov jobs vs the coverage-free forky combos\njob, where ML-DSA-style rows belong), the gcov/#ifdef coverage-visibility note,\nand the runtime capability-probe testing pattern.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "afe663c065d7a6b3d3c9deac30e9cb0e624b032c",
      "tree": "32efe7432910fefc5a2b5ba04259defd4e0e0df0",
      "parents": [
        "67b81e3fa2102b10dd671cea5951f17e3bce06c9",
        "22c6b288dc503679ceaed6f5f47f0c83af0b7aff"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Tue Jun 16 08:07:00 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 16 08:07:00 2026 -0400"
      },
      "message": "Merge pull request #303 from benmcollins/301-mldsa-gnutls\n\nAdd GnuTLS ML-DSA (FIPS 204 / RFC 9964) backend"
    },
    {
      "commit": "22c6b288dc503679ceaed6f5f47f0c83af0b7aff",
      "tree": "32efe7432910fefc5a2b5ba04259defd4e0e0df0",
      "parents": [
        "6056dfc9cee84b7b9f2f691ee6264a081ce6b296"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 07:47:57 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Tue Jun 16 07:47:57 2026 -0400"
      },
      "message": "ci: exercise ML-DSA on the forky (OpenSSL 3.6.2) build\n\nAdd a WITH_ML_DSA\u003dON row to the build-linux-combos matrix. Debian forky ships\nOpenSSL \u003e\u003d 3.5, so this build actually runs the ML-DSA sign/verify/export tests\nagainst a capable backend in CI (previously the ML-DSA code was only compiled\nlocally). The jwt_mldsa tests probe each backend at runtime, so the OpenSSL\nsuccess path is exercised while the GnuTLS/MbedTLS rejection paths are checked\ntoo. This row is in the coverage-free combos job, so the GnuTLS ML-DSA lines\n(compiled but unexercised without a leancrypto-enabled GnuTLS) do not affect\nCodecov.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "6056dfc9cee84b7b9f2f691ee6264a081ce6b296",
      "tree": "44534c6057ca5762c782de3ea8190992cf0f1f4e",
      "parents": [
        "67b81e3fa2102b10dd671cea5951f17e3bce06c9"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 22:14:18 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 22:14:18 2026 -0400"
      },
      "message": "feat: add GnuTLS ML-DSA (FIPS 204 / RFC 9964) backend\n\nImplement the GnuTLS side of ML-DSA, deferred from #227. GnuTLS does ML-DSA\nwhen built against a PQC provider (e.g. --with-leancrypto); it is enabled when\nGnuTLS \u003e\u003d 3.8.10 is present and WITH_ML_DSA is set.\n\nGnuTLS has no raw ML-DSA import, so gnutls_process_mldsa() builds the encoded\nkey from the JWK\u0027s raw \"pub\" / 32-byte \"priv\" seed (a SubjectPublicKeyInfo for\npublic keys, a seed-form PKCS#8 for private keys) and imports that. Signing\nuses gnutls_privkey_sign_data2() / gnutls_pubkey_verify_data2() with\nGNUTLS_SIGN_MLDSA44/65/87; the sign path pins the expected GNUTLS_PK_MLDSA*\nso a mismatched-variant key is rejected. Native-key -\u003e JWK export recovers the\nraw public key from the SPKI and the 32-byte seed from a seed-form PKCS#8.\n\n- A seedless (expanded-only) private key has no FIPS-204 seed and cannot be a\n  private AKP JWK. GnuTLS\u0027s seed PKCS#8 export null-derefs on such a key, so\n  export probes for the seed via a plain PKCS#8 export first and downgrades to\n  a public export when absent (matching the OpenSSL backend) instead of\n  crashing.\n- The private seed is wiped from its stack buffer after import.\n\nLIBJWT_HAVE_ML_DSA is now defined for OpenSSL \u003e\u003d 3.5 OR GnuTLS \u003e\u003d 3.8.10, and\neach backend\u0027s ML-DSA code carries its own version guard so a build can enable\nthe feature via either backend independently. MbedTLS still has no ML-DSA and\nrejects AKP keys via the common null-guard.\n\nThe ML-DSA tests gain a runtime capability probe so the sign/verify/export and\nseedless-downgrade tests run on every ML-DSA-capable backend (OpenSSL and a\nPQC-enabled GnuTLS) and skip the rest. README and the public header note the\nGnuTLS path.\n\ncloses #301\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "67b81e3fa2102b10dd671cea5951f17e3bce06c9",
      "tree": "c0c1d1996d9aeffd27a892c3bdffd1a288b99bbe",
      "parents": [
        "bd0270c8577c9ef1cc3d0a68723fdc345ade621c",
        "cfee8fd9fbd971bc214cfb887289766ceca35306"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 20:38:55 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 20:38:55 2026 -0400"
      },
      "message": "Merge pull request #302 from benmcollins/227-ml-dsa-support\n\nAdd experimental ML-DSA (FIPS 204 / RFC 9964) signing"
    },
    {
      "commit": "cfee8fd9fbd971bc214cfb887289766ceca35306",
      "tree": "ff02269afcdea9e8883c6f5c620764721e5bdb81",
      "parents": [
        "f9c845e317b0793da705d1c4bc0f72f9a96e105e"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 20:17:07 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 20:17:07 2026 -0400"
      },
      "message": "feat: add experimental ML-DSA (FIPS 204 / RFC 9964) signing\n\nAdd support for the post-quantum ML-DSA signature algorithms (ML-DSA-44,\nML-DSA-65, ML-DSA-87) registered for JOSE by RFC 9964. ML-DSA keys use the\nnew \"AKP\" (Algorithm Key Pair) JWK key type with a \"pub\" member and a \"priv\"\nmember holding the 32-byte FIPS-204 seed; \"alg\" is required on every AKP key.\n\nThe feature is experimental and off by default: build with -DWITH_ML_DSA\u003dON.\nIt is only enabled when a backend that implements ML-DSA is present (OpenSSL\n\u003e\u003d 3.5 today), and the public header then defines LIBJWT_HAVE_ML_DSA so\ndownstream code can detect it. The JWT_ALG_ML_DSA_* and JWK_KEY_TYPE_AKP enum\nvalues are always present (ABI), but all behavior is gated on the macro.\n\n- OpenSSL: real sign/verify (one-shot, like EdDSA), AKP JWK parse via the\n  seed, and native-key -\u003e JWK export. A seedless private key (one with no\n  retained FIPS-204 seed) downgrades to a clean public export rather than a\n  private JWK with no private material.\n- GnuTLS / MbedTLS: cleanly reject AKP keys at parse time via a null-guard on\n  jwt_ops-\u003eprocess_mldsa (a real GnuTLS implementation is tracked in #301).\n- Anti algorithm-confusion: ML-DSA maps to JWK_KEY_TYPE_AKP only, and the\n  variant is pinned to the key, so a 44 key cannot be used for an 87 token.\n\nTests cover sign/verify and the emitted header alg for all three variants,\nAKP parse-error branches, the non-OpenSSL reject path, native PEM/DER -\u003e JWK\nexport (including the seedless downgrade), public-export priv-stripping, and\nthe CLI tools, under both Jansson and json-c.\n\ncloses #227\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "bd0270c8577c9ef1cc3d0a68723fdc345ade621c",
      "tree": "dcf8d78ec231048eed0f9d12bc8d2395cb6a98f4",
      "parents": [
        "f9c845e317b0793da705d1c4bc0f72f9a96e105e"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 18:00:27 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 18:00:27 2026 -0400"
      },
      "message": "Update logo image in README.md\n\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e"
    },
    {
      "commit": "f9c845e317b0793da705d1c4bc0f72f9a96e105e",
      "tree": "e353e9726b25c701c97d35c7315b6ee6eff63602",
      "parents": [
        "a00c9ddf2c9734c3ea7ff9da2eaa415c47f7f7d3"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 17:57:24 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 17:57:24 2026 -0400"
      },
      "message": "docs: add GitHub social preview card (1280x640)\n\nAdd a social preview image for the repository, built around the LibJWT\ntoken-medallion logo in the project\u0027s gold-on-dark palette. The card\nshows the wordmark, the JOSE standards (JWS/JWE/JWK/JWA/JWT), a\ncolor-coded header.payload.signature JWT, and the crypto backends.\n\nimages/social-preview.svg is the self-contained, editable source (coin\nembedded as a data-URI); regenerate the PNG with:\n\n  rsvg-convert -w 1280 -h 640 images/social-preview.svg -o images/social-preview.png\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "a00c9ddf2c9734c3ea7ff9da2eaa415c47f7f7d3",
      "tree": "ab0374747a05ec574d7afef7b4793b106e1b99d9",
      "parents": [
        "6e5f475c8281aace1be3785fa625e331e510537f"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 16:35:32 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 16:35:32 2026 -0400"
      },
      "message": "ci: move workflow actions off the deprecated Node.js 20 runtime\n\nGitHub is deprecating the Node 20 actions runtime. Bump every action\nthat runs on (or internally invokes) Node 20 to a Node 24 release:\n\n  - actions/checkout              v4 -\u003e v5\n  - actions/deploy-pages          v4 -\u003e v5\n  - actions/upload-pages-artifact v3 -\u003e v5  (now pulls upload-artifact v7)\n\nthreeal/cmake-action has no Node 24 release yet (only an unreleased main;\nupstream issue threeal/cmake-action#883), so replace its three uses with\nthe equivalent raw cmake invocations already used by the combos/mbedtls\njobs, dropping the dependency entirely.\n\nLeft unchanged (not on Node 20): github/codeql-action@v4 (already node24),\ncodecov/codecov-action@v5.1.2 and ConorMacBride/install-package@v1 (both\npure-shell composite actions with no Node sub-actions).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "6e5f475c8281aace1be3785fa625e331e510537f",
      "tree": "0f53f34b0e72575c00d8159d6106d92307f5ee8e",
      "parents": [
        "5a705e69924ac97afb0c260462b969bd68ea1855"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 16:18:14 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 16:18:14 2026 -0400"
      },
      "message": "gnutls: drop the RSA-OAEP (SHA-1) OpenSSL fallback; just fail\n\nGnuTLS/nettle cannot perform RSA-OAEP with SHA-1. The GnuTLS backend used to\ndelegate that case to OpenSSL when it was compiled in; now it simply rejects\nplain RSA-OAEP (RSA-OAEP-256 remains native). This makes the GnuTLS backend\u0027s\nbehavior independent of whether OpenSSL is also built.\n\n- gnutls/jwe.c: RSA-OAEP (SHA-1) encrypt/decrypt return failure instead of\n  calling openssl_*_cek_rsa_pem.\n- Remove the now-dead openssl_encrypt_cek_rsa_pem / openssl_decrypt_cek_rsa_pem\n  helpers (and their declarations); they existed solely for this fallback. The\n  shared rsa_oaep_{encrypt,decrypt}_pkey helpers stay (used by the OpenSSL ops);\n  their comments are updated.\n- tests/jwe_rsa.c: the GnuTLS backend never supports plain RSA-OAEP now,\n  regardless of OpenSSL.\n- README and doxygen/mainpage.dox: mark RSA-OAEP (SHA-1) unsupported under\n  GnuTLS, drop the now-unused \"OpenSSL fallback\" legend entry, and update the\n  note.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "5a705e69924ac97afb0c260462b969bd68ea1855",
      "tree": "1c7ef1f21237600f468854a75d5f31d9f1044d4f",
      "parents": [
        "3974c56eaa6d4737df1ab3bb9c7e0b968f29bc79"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 15:36:13 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 15:36:13 2026 -0400"
      },
      "message": "gnutls: require GnuTLS \u003e\u003d 3.8.8 for issue-free EdDSA Ed448\n\nGnuTLS gained reliable Ed448 support in 3.8.8, so raise the minimum from\n3.8.4 to 3.8.8. With the floor at 3.8.8, Ed448 is always available under the\nGnuTLS backend, so drop the now-redundant \"\u003e\u003d 3.8.8\" qualifier on the Ed448\nrow of the algorithm-support matrix in the README and doxygen/mainpage.dox.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "3974c56eaa6d4737df1ab3bb9c7e0b968f29bc79",
      "tree": "1d9ff36ce5a7f53419305c229d07a47274e00034",
      "parents": [
        "ee2bf9cfa9fc3faaf3c56506f4c06c4c4babddcb"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 15:15:04 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 15:15:04 2026 -0400"
      },
      "message": "gnutls: require GnuTLS \u003e\u003d 3.8.4 and drop pre-3.8.4 compatibility\n\nGnuTLS \u003c 3.8.4 lacks the SPKI-OAEP and raw-ECDH APIs, so those builds parsed\nJWKs and did RSA-OAEP/ECDH-ES via an OpenSSL fallback. Now that the native\npath is the only supported one, raise the floor to GnuTLS \u003e\u003d 3.8.4 and remove\nthe fallback machinery:\n\n- CMakeLists.txt: pkg_check_modules floor 3.6.0 -\u003e 3.8.4; drop the now-dead\n  \"GnuTLS \u003c 3.8.4 needs OpenSSL\" FATAL_ERROR guard.\n- Remove the JWT_GNUTLS_NATIVE_JWE macro and all its #if/#else gating; the\n  native gnutls_process_*/JWE ops are now unconditional.\n- Drop the openssl_process_*/openssl_{encrypt,decrypt}_cek_rsa/openssl_ecdh_derive\n  fallback declarations and ops-table entries.\n\nThe RSA-OAEP (SHA-1) -\u003e OpenSSL fallback is unchanged: GnuTLS/nettle still\ncannot do SHA-1 OAEP, so it delegates to OpenSSL when compiled in (and is\nrejected cleanly otherwise). README and doxygen/mainpage.dox updated.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "ee2bf9cfa9fc3faaf3c56506f4c06c4c4babddcb",
      "tree": "3daf1bba242c5f8409332a5fa37c932e2154e2d0",
      "parents": [
        "3e235b1ca00b3f34dd7abfd8151ffdef78f6f1d7"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 15:05:08 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 15:05:08 2026 -0400"
      },
      "message": "docs: replace em-dash with colon in the JSON-library line\n\nThe em-dash rendered as an artifact on the Doxygen-derived pages. Use a plain\ncolon in the README and mainpage.dox \"Required\" lists.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "3e235b1ca00b3f34dd7abfd8151ffdef78f6f1d7",
      "tree": "9190effc22aa37fd68de9d2c2847637d4d52a4f8",
      "parents": [
        "d19efd19132a490aad0b7ca301206cfb7bad0acf",
        "1d9185293b3417d7973a4216344a5cd2c1eb5995"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 14:58:40 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 14:58:40 2026 -0400"
      },
      "message": "Merge pull request #300 from benmcollins/299-docs-optional-backends\n\ndocs: OpenSSL and JSON library are optional/interchangeable (closes #299)"
    },
    {
      "commit": "1d9185293b3417d7973a4216344a5cd2c1eb5995",
      "tree": "9190effc22aa37fd68de9d2c2847637d4d52a4f8",
      "parents": [
        "d19efd19132a490aad0b7ca301206cfb7bad0acf"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 14:57:42 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 14:57:42 2026 -0400"
      },
      "message": "docs: OpenSSL and the JSON library are optional/interchangeable (closes #299)\n\n#297 made OpenSSL an optional crypto backend, but doxygen/mainpage.dox still\nsaid \"OpenSSL is always required\". Fix that note (and align the README) to\nstate that any non-empty combination of {OpenSSL, GnuTLS, MbedTLS} works,\nOpenSSL is opt-out via -DWITH_OPENSSL\u003dOFF, and a GnuTLS-only build needs\nGnuTLS \u003e\u003d 3.8.4. Also note the GnuTLS RSA-OAEP (SHA-1) -\u003e OpenSSL fallback\nonly applies when OpenSSL is compiled in.\n\nAdditionally document that the JSON library is interchangeable: Jansson\n(\u003e\u003d 2.0) by default or json-c (\u003e\u003d 0.16) via -DWITH_JSON_C\u003dON.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "d19efd19132a490aad0b7ca301206cfb7bad0acf",
      "tree": "78d05e6e4892ea6f1471b4e6faf0933b93f345b6",
      "parents": [
        "ac01b6ed021ae8752dcf957af0edd6749dfe4488",
        "ddd207519c517eed7ce988db357e3cfa65efadfb"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 14:52:29 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 14:52:29 2026 -0400"
      },
      "message": "Merge pull request #298 from benmcollins/297-openssl-optional\n\nMake OpenSSL an optional crypto backend (closes #297)"
    },
    {
      "commit": "ddd207519c517eed7ce988db357e3cfa65efadfb",
      "tree": "78d05e6e4892ea6f1471b4e6faf0933b93f345b6",
      "parents": [
        "a9ff19f69ae7cddaf3b70a9543d1950df57c8d86"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 14:29:41 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 14:29:41 2026 -0400"
      },
      "message": "tests: skip jwk2key RSA PEM byte-compare on non-PKCS#8 backends\n\nThe \"Convert JWK to PEM - RSA\" CLI test compares jwk2key output byte-for-byte\nagainst a PKCS#8 golden file. OpenSSL and GnuTLS emit PKCS#8 (\"BEGIN PRIVATE\nKEY\"), but MbedTLS emits the equally-valid PKCS#1 (\"BEGIN RSA PRIVATE KEY\"),\nso the comparison cannot match under a MbedTLS-only build. Skip it when the\nproduced PEM is not PKCS#8. Exposed now that MbedTLS-only builds are possible.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "a9ff19f69ae7cddaf3b70a9543d1950df57c8d86",
      "tree": "63daf734e02e854b2376af16da350ff696ddaa14",
      "parents": [
        "39fef0d3c70b7e9a096437d519d765ebd1a6e5ba"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 14:10:22 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 14:10:22 2026 -0400"
      },
      "message": "key2jwk: fix builds on MbedTLS 3.x and GnuTLS \u003c 3.8.4\n\n- MbedTLS 3.x has no mbedtls_pk_get_key_type() (added in 4.x); fall back to\n  the classic mbedtls_pk_get_type()/mbedtls_pk_type_t for key-type detection\n  under MBEDTLS_VERSION_MAJOR \u003c 4.\n- The GnuTLS backend has no secp256k1 in its native key2jwk regardless of\n  GnuTLS version, so skip that fixture for the GnuTLS backend unconditionally\n  (the old gate keyed on the \u003e\u003d 3.8.4 native-JWE threshold, which let it run\n  on Ubuntu\u0027s GnuTLS 3.8.3 and fail). Drop the now-unused gnutls_native_jwk\n  helper and its gnutls.h include from the test.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "39fef0d3c70b7e9a096437d519d765ebd1a6e5ba",
      "tree": "64152267a8345208a52841f1169e0e8de0c9d3cf",
      "parents": [
        "ac01b6ed021ae8752dcf957af0edd6749dfe4488"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 13:52:41 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 13:52:41 2026 -0400"
      },
      "message": "Make OpenSSL an optional crypto backend (closes #297)\n\nOpenSSL was mandatory; it is now one of three interchangeable backends.\nlibjwt builds and passes its test suite with any non-empty subset of\n{OpenSSL, GnuTLS, MbedTLS} (default WITH_OPENSSL\u003dON).\n\nCore decoupling:\n- Replace the OPENSSL_cleanse-based jwt_scrub_and_free (used in ~60 sites\n  across the core and every backend) with a portable jwt_cleanse; guard the\n  \u003copenssl/crypto.h\u003e include under HAVE_OPENSSL and include \u003climits.h\u003e\n  explicitly so non-OpenSSL builds need no OpenSSL header.\n\nNative key2jwk:\n- Split openssl/jwk-export.c into a crypto-free common jwk-export.c (the new\n  jwk_export_t struct + key2jwk_params ops member, JWK assembly, base64url\n  encoding, kid via the backend RNG, and the HMAC fallback) plus native\n  per-backend extractors for OpenSSL, GnuTLS, and MbedTLS. Drops the\n  cross-backend dependency on openssl_key2jwk.\n\nRSA-OAEP (SHA-1):\n- The GnuTLS -\u003e OpenSSL fallback for plain RSA-OAEP (which GnuTLS/nettle\n  cannot perform), is now gated on HAVE_OPENSSL; without OpenSSL it fails\n  cleanly instead of referencing an undefined symbol.\n\nBuild:\n- Add WITH_OPENSSL (default ON); move OpenSSL detection, sources, links, and\n  HAVE_CRYPTO inside if(WITH_OPENSSL); stop linking OpenSSL into the tools;\n  make the pkg-config Requires conditional; require GnuTLS \u003e\u003d 3.8.4 for an\n  OpenSSL-less GnuTLS build (older GnuTLS has no native JWK/JWE path).\n\nCI:\n- Add a build-linux-combos matrix exercising all seven backend combinations\n  (build + full test suite) in a debian:forky container.\n\nTests cover each backend\u0027s native key2jwk, with skips for genuinely\nunsupported cases (MbedTLS+EdDSA/RSA-PSS, native GnuTLS+secp256k1); the CLI\ngolden-JWKS test skips on backends that cannot convert every key type.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "ac01b6ed021ae8752dcf957af0edd6749dfe4488",
      "tree": "b78c9fc8a26a07058c61e428690e8445ee94fbc2",
      "parents": [
        "f9a05a8636d25e07218d8e443b849b981138e515",
        "d8056594b1198fc41d73e5d0cdd57bec264c79c8"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 10:00:22 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 10:00:22 2026 -0400"
      },
      "message": "Merge pull request #296 from benmcollins/267-mbedtls-4x\n\nmbedtls: migrate the native backend to the PSA Crypto API (4.x support)"
    },
    {
      "commit": "d8056594b1198fc41d73e5d0cdd57bec264c79c8",
      "tree": "b78c9fc8a26a07058c61e428690e8445ee94fbc2",
      "parents": [
        "f9a05a8636d25e07218d8e443b849b981138e515"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 09:45:37 2026 -0400"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 09:45:37 2026 -0400"
      },
      "message": "mbedtls: migrate the native backend to the PSA Crypto API (closes #267)\n\nMbedTLS 4.x (TF-PSA-Crypto) split the crypto core out, made the classic\nlow-level API (mbedtls_rsa_*, mbedtls_ecp_*, mbedtls_gcm_*, ...) private\nunder mbedtls/private/, removed the low-level mbedtls_ecdh_* primitives\nentirely, and reworked even some public APIs (mbedtls_md HMAC, nist_kw)\ntoward PSA. Reaching into the private headers would just re-break on the\nnext release, so move the whole native backend onto the public PSA Crypto\nAPI (psa/crypto.h). PSA is also stable in MbedTLS 3.6 LTS, so this is a\nsingle code path for both lines with no version #ifdefs; the CMake floor\nstays mbedcrypto \u003e\u003d 3.6.0.\n\nKey model: a parsed JWK now holds PSA-importable material (RSA PKCS#1 DER\nbuilt via the public asn1write.h; EC/OKP-X curve family + raw point/scalar)\nrather than a native key object. Each operation imports a short-lived\nvolatile PSA key with exactly the policy it needs, then destroys it. This\nsidesteps PSA\u0027s single-policy-per-key model and keeps RSA parse-time\nleniency.\n\n  - sign/verify: psa_sign_message/psa_verify_message (RSA PSS/PKCS1v15,\n    ECDSA); HMAC via PSA MAC. Strict signature-size checks preserved.\n  - JWE: GCM via PSA AEAD, CBC via PSA cipher + truncated-MAC HMAC,\n    RSA-OAEP via PSA, RNG via psa_generate_random, ECDH via\n    psa_raw_key_agreement, and RFC 3394 AES-KW hand-rolled on PSA AES-ECB\n    (nist_kw changed shape in 4.x). EdDSA stays unsupported.\n  - EC keys are validated at parse via a throwaway PSA import; item-\u003ebits\n    is computed at parse.\n\nTwo PSA constraints handled: psa_generate_key cannot produce Montgomery\npairs (X25519/X448 ephemerals generate a random scalar and import it), and\nsecp256k1 is rejected for ECDH-ES (PSA would otherwise allow it).\n\nPSA caps RSA at PSA_VENDOR_RSA_MAX_KEY_BITS (4096 by default), which the\nclassic API did not. Oversize RSA keys are now rejected at parse with a\nclear message; the keyring and flip-flop tests skip the 8192-bit key on\nthe MbedTLS backend, mirroring the existing EdDSA skip.\n\nVerified against MbedTLS 4.1: full suite passes (mbedtls-only and the\nOpenSSL+GnuTLS+MbedTLS+libcurl combo) with no new uncovered lines.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "f9a05a8636d25e07218d8e443b849b981138e515",
      "tree": "54cdc36f7758eb22ee428e673cf90a08ce8147a1",
      "parents": [
        "6ea3b6b341196c146e434fe154b3d44c86e78ede"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 12:09:25 2026 +0000"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 12:09:25 2026 +0000"
      },
      "message": "Update copyright notices to 2026 in files edited this year\n\nAll source files (and CMakeLists.txt) touched by 2026 commits had a\nmaClara copyright notice ending in 2025. Extend each to 2026.\n\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "6ea3b6b341196c146e434fe154b3d44c86e78ede",
      "tree": "fc1b6eb5bd56f0820ec7a7f1195cf7a0b6f8313d",
      "parents": [
        "b28c5a3bd47f20335249ba3c19e51a283254f570",
        "08f5813f1bebf69ed88858d7030be66ad144c1e8"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 08:03:36 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 08:03:36 2026 -0400"
      },
      "message": "Merge pull request #295 from benmcollins/release-3.4.0\n\nRelease v3.4.0"
    },
    {
      "commit": "08f5813f1bebf69ed88858d7030be66ad144c1e8",
      "tree": "fc1b6eb5bd56f0820ec7a7f1195cf7a0b6f8313d",
      "parents": [
        "b28c5a3bd47f20335249ba3c19e51a283254f570"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:41:39 2026 +0000"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:41:39 2026 +0000"
      },
      "message": "Release v3.4.0\n\nBump version 3.3.3 -\u003e 3.4.0 and the SONAME triple (libtool c:r:a)\n16:7:2 -\u003e 17:0:3. This release only adds interfaces (38 new public\nsymbols; none removed or changed), so per the libtool rules this is\ncurrent++, revision\u003d0, age++ — the SONAME stays libjwt.so.14 and\nexisting binaries remain compatible.\n\nAlso drop the now-stale \"Version 3 is a complete overhaul\" warning\nfrom the documentation main page.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) \u003cnoreply@anthropic.com\u003e\nSigned-off-by: Ben Collins \u003cbcollins@libjwt.io\u003e\n"
    },
    {
      "commit": "b28c5a3bd47f20335249ba3c19e51a283254f570",
      "tree": "c72660def3f879e4385474ca316216aa8c09ce05",
      "parents": [
        "51c2efce66840aba937311644ae62b84c891bc67",
        "4859ab4c9c90ec974d9d4d88f4d1195b2888963e"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 07:19:47 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 07:19:47 2026 -0400"
      },
      "message": "Merge pull request #293 from benmcollins/284-strict-base64url\n\njwt: enforce strict base64url on decode; fix non-conformant keys (L16, #284)"
    },
    {
      "commit": "4859ab4c9c90ec974d9d4d88f4d1195b2888963e",
      "tree": "c72660def3f879e4385474ca316216aa8c09ce05",
      "parents": [
        "d180cc752bc3a04be866923863012b74952d78b3",
        "51c2efce66840aba937311644ae62b84c891bc67"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:19:38 2026 +0000"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:19:38 2026 +0000"
      },
      "message": "Merge branch \u0027master\u0027 into 284-strict-base64url\n\n# Conflicts:\n#\ttests/jwt_security.c\n"
    },
    {
      "commit": "51c2efce66840aba937311644ae62b84c891bc67",
      "tree": "94dc720c14d8034ee5d7382fb91028d68b36e532",
      "parents": [
        "e7d642e70e2f5c2966ae934c1330cd35f4d63b41",
        "ebe7fe242e1d2417776caf74dd0696b6c3cad17a"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 07:18:04 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 07:18:04 2026 -0400"
      },
      "message": "Merge pull request #292 from benmcollins/283-claim-validation\n\nverify: claim-validation \u0026 JOSE conformance (L5-L9/L17, #283)"
    },
    {
      "commit": "ebe7fe242e1d2417776caf74dd0696b6c3cad17a",
      "tree": "94dc720c14d8034ee5d7382fb91028d68b36e532",
      "parents": [
        "18133e4fbdd613367c3d03e3b026a2ae5ebbd2b5",
        "e7d642e70e2f5c2966ae934c1330cd35f4d63b41"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:17:55 2026 +0000"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:17:55 2026 +0000"
      },
      "message": "Merge branch \u0027master\u0027 into 283-claim-validation\n\n# Conflicts:\n#\ttests/jwe_gcm.c\n#\ttests/jwt_security.c\n"
    },
    {
      "commit": "e7d642e70e2f5c2966ae934c1330cd35f4d63b41",
      "tree": "cf9b4d7d3923324ace03b7b6832b5ab2043881fe",
      "parents": [
        "fa801778784fc76e32a01526ffe893bfe06b47b8",
        "b259915c68e0ba1d4df80967024e14cc509eb700"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 07:14:47 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 07:14:47 2026 -0400"
      },
      "message": "Merge pull request #291 from benmcollins/282-jwks-curl-hardening\n\njwks: stream curl response with a bounded growing buffer (L10/L11/L15, #282)"
    },
    {
      "commit": "b259915c68e0ba1d4df80967024e14cc509eb700",
      "tree": "cf9b4d7d3923324ace03b7b6832b5ab2043881fe",
      "parents": [
        "7bd7ad930690dc410bc2dff77488789115295b87",
        "fa801778784fc76e32a01526ffe893bfe06b47b8"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:14:38 2026 +0000"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:14:38 2026 +0000"
      },
      "message": "Merge branch \u0027master\u0027 into 282-jwks-curl-hardening\n\n# Conflicts:\n#\tlibjwt/jwks-curl.c\n"
    },
    {
      "commit": "fa801778784fc76e32a01526ffe893bfe06b47b8",
      "tree": "4336640c4860f627bf144862d647495140772c65",
      "parents": [
        "1b7f3b08db16ada2f000d2b5c158bdd8dc7e5cf6",
        "a0588d13fd3cc61012021325102bddf16f75f176"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 07:13:27 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 07:13:27 2026 -0400"
      },
      "message": "Merge pull request #290 from benmcollins/281-crypto-backend-hardening\n\ncrypto: IV/ECDSA/OKP/verify-return strictness (L1-L4, #281)"
    },
    {
      "commit": "a0588d13fd3cc61012021325102bddf16f75f176",
      "tree": "4336640c4860f627bf144862d647495140772c65",
      "parents": [
        "2ef95006024686bdf8fc314a5c4785d5f2bdb615",
        "1b7f3b08db16ada2f000d2b5c158bdd8dc7e5cf6"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:13:18 2026 +0000"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:13:18 2026 +0000"
      },
      "message": "Merge branch \u0027master\u0027 into 281-crypto-backend-hardening\n\n# Conflicts:\n#\ttests/jwt_security.c\n"
    },
    {
      "commit": "1b7f3b08db16ada2f000d2b5c158bdd8dc7e5cf6",
      "tree": "855e721587dc051af8c4878bb581060c6ffe2d73",
      "parents": [
        "6fe630965159d91a562a597c55b35dabd3333ca3",
        "930ede3dd9f3c5b382a9339075ac9664399b69eb"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "benmcollins13@gmail.com",
        "time": "Mon Jun 15 07:10:23 2026 -0400"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 07:10:23 2026 -0400"
      },
      "message": "Merge pull request #289 from benmcollins/282-jwks-oom-free\n\njwks: free owned item, not borrowed jwk, on clone-OOM (M3, #282)"
    },
    {
      "commit": "930ede3dd9f3c5b382a9339075ac9664399b69eb",
      "tree": "855e721587dc051af8c4878bb581060c6ffe2d73",
      "parents": [
        "60902e1ee6831209b388ff81a8efa9ef1f3d964a",
        "6fe630965159d91a562a597c55b35dabd3333ca3"
      ],
      "author": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:10:14 2026 +0000"
      },
      "committer": {
        "name": "Ben Collins",
        "email": "bcollins@libjwt.io",
        "time": "Mon Jun 15 11:10:14 2026 +0000"
      },
      "message": "Merge branch \u0027master\u0027 into 282-jwks-oom-free\n\n# Conflicts:\n#\ttests/jwt_security.c\n"
    }
  ],
  "next": "6fe630965159d91a562a597c55b35dabd3333ca3"
}
