Hello, cjose 0.6.2.1-1+deb12u2 for bookworm-security is ready for review. The debdiff against 0.6.2.1-1+deb12u1 is attached.
It carries the same two fixes DSA-6499-1 shipped in cjose 0.6.2.3-1+deb13u1 in Trixie, and cjose 0.6.2.8-1 in Sid has the same two commits: - CVE-2026-53938, https://github.com/OpenIDC/cjose/security/advisories/GHSA-75r7-f5cv-g3wj - CVE-2026-53939, https://github.com/OpenIDC/cjose/security/advisories/GHSA-f6wf-pqg3-6wqq Thanks to Ivo Marino, who posted the first Bookworm backport of these two to the list in September. The first one is a heap overflow. 0.6.2.1-1+deb12u1 copies an AES key wrap into a fixed buffer, so an oversized wrapped key writes past the end of it. deb12u2 checks the length first and returns an error if the wrapped key is too long. The second one is in the same 0.6.2.1 code. It fills the AES-CBC-HMAC content-encryption key with zeros, so two encryptions under the same wrapping key come out identical, and the payload ends up under a key anyone can know. With the fix, a fresh key is drawn every time, so two encryptions now come out different. Tokens made under the old all-zero key still decrypt, since that key rides along inside the token. The heap-overflow commit also touches CHANGELOG.md for a later release. I left the 0.6.2.1 release notes alone, and put the new test after test_cjose_jwe_decrypt_aes_gcm, which deb12u1 already has. For testing, I built this in a Bookworm schroot and also built both versions with AddressSanitizer. On deb12u1, the two AES-CBC-HMAC encryptions match, and a 64-byte wrapped key makes AES_unwrap_key write 56 bytes at the end of the 16-byte content-encryption key. That write is the wrapped length minus the 8-byte wrap overhead, so a 200-byte wrapped key is a 192-byte write of the same bug. On deb12u2, the encrypted keys differ and the oversized wrap comes back as CJOSE_ERR_INVALID_ARG. A token the older build encrypted with A128KW and A128CBC-HS256 still decrypts, and so does one it made with dir and A256GCM. The upstream suite runs 80 checks with 0 failures, both on the sanitizer build and on the sbuild binary, and a normal A128KW/A128GCM round trip works. libcjose0 0.6.2.1-1+deb12u1 has four reverse dependencies in Bookworm. I ran each of them in that schroot against both deb12u1 and deb12u2. libapache2-mod-auth-openidc 2.4.12.3-2+deb12u5 signs and verifies tokens, decrypts incoming JWE, and encrypts a request object. If you set the encryption algorithm and leave the content encryption empty, it picks A128CBC-HS256, which is the zero-key path. I built the Bookworm source and ran test/test (the same program make check runs), and with both libraries it printed "All 25 tests passed!" That run decrypts the AES key wrap and A128CBC-HS256 sample ("Live long and prosper."), decrypts the nested JWT, and signs and verifies an RS256 token. apache2ctl -M lists auth_openidc_module with both. liboauth2 1.4.5.4-1 encrypts with dir and A256GCM, and libapache2-mod-oauth2 3.3.1-1 reaches cjose through it. I built check_liboauth2 against the packaged liboauth2.so.0 and ran CK_RUN_SUITE=jose. With both libraries, the hash check, the symmetric-key check, encrypt, decrypt of the saved dir/A256GCM samples, the plain JWK check, and JWT verify all pass. test_jwks_resolve_uri fails on both, because it fetches http://127.0.0.1:8080/test/check_jose/jwks_uri and nginx on my machine already answers that port. apache2ctl -M lists oauth2_module with both. trafficserver-experimental-plugins 9.2.5+ds-0+deb12u4 (amd64 and arm64) links libcjose.so.0 from uri_signing.so, which signs and verifies JWS. I ran traffic_server from trafficserver 9.2.5+ds-0+deb12u4 on 127.0.0.1:18080, with uri_signing.so pointed at an HS256 key for issuer CDN and a local origin on 127.0.0.1:18081. With both libraries, a request with no token gets a 403, and one carrying URISigningPackage set to an HS256 JWS for that key gets a 200 with the origin body. ocserv 1.1.6-3 build-depends on libcjose-dev, and the Debian build leaves OpenID Connect auth at the upstream default (off), so the installed ocserv, ocserv-worker, ocpasswd, and occtl stop at their other libraries. To exercise the cjose path, I rebuilt the Bookworm source with --enable-oidc-auth and ran tests/test-oidc, which signs ES256 bearer tokens and posts them to a local ocserv. With both libraries, the good token gets a 200 and the twelve bad tokens get a 401. After the suite rotates the signing key, the old good token gets a 401 and a fresh one gets a 200. Here is the Salsa fork: https://salsa.debian.org/tsimonq2/cjose/-/tree/bookworm Here is the Debusine work request: https://debusine.debian.net/debian/developers/work-request/1578381/ Feedback is welcome. If nothing showstopping comes up, I'll upload this and issue the DLA on Saturday morning (November 7th, 2026). Best regards, Simon Quigley [email protected]
diff -Nru cjose-0.6.2.1/debian/changelog cjose-0.6.2.1/debian/changelog --- cjose-0.6.2.1/debian/changelog 2023-08-04 09:06:39.000000000 -0500 +++ cjose-0.6.2.1/debian/changelog 2026-11-02 14:09:39.000000000 -0600 @@ -1,3 +1,11 @@ +cjose (0.6.2.1-1+deb12u2) bookworm-security; urgency=high + + * Non-maintainer upload by the LTS Team. + * CVE-2026-53938: heap buffer overflow in AES key unwrap. + * CVE-2026-53939: all-zero content-encryption key for AES-CBC-HMAC. + + -- Simon Quigley <[email protected]> Mon, 02 Nov 2026 14:09:39 -0600 + cjose (0.6.2.1-1+deb12u1) bookworm-security; urgency=medium * CVE-2023-37464 (Closes: #1041423) diff -Nru cjose-0.6.2.1/debian/patches/CVE-2026-53938.patch cjose-0.6.2.1/debian/patches/CVE-2026-53938.patch --- cjose-0.6.2.1/debian/patches/CVE-2026-53938.patch 1969-12-31 18:00:00.000000000 -0600 +++ cjose-0.6.2.1/debian/patches/CVE-2026-53938.patch 2026-11-02 12:05:47.000000000 -0600 @@ -0,0 +1,131 @@ +Origin: https://github.com/OpenIDC/cjose/commit/8c51d245273583a658f24ef7b08ba22f848a34a5 +Reviewed-by: Simon Quigley <[email protected]> +Last-Update: 2026-11-02 + +Backported to 0.6.2.1. The CHANGELOG.md hunk is later release notes, +and the new test is registered after test_cjose_jwe_decrypt_aes_gcm. + +From 8c51d245273583a658f24ef7b08ba22f848a34a5 Mon Sep 17 00:00:00 2001 +From: Hans Zandbelt <[email protected]> +Date: Tue, 2 Jun 2026 11:22:36 +0200 +Subject: [PATCH] fix heap buffer overflow in AES key unwrap + +by validating the encrypted_key length before AES_unwrap_key + +Signed-off-by: Hans Zandbelt <[email protected]> +--- +diff --git a/src/jwe.c b/src/jwe.c +--- a/src/jwe.c ++++ b/src/jwe.c +@@ -587,6 +587,15 @@ + return false; + } + ++ // the wrapped key (RFC 3394) is always the plaintext CEK length plus 8 bytes; ++ // enforce this before calling AES_unwrap_key, which would otherwise copy the ++ // attacker-controlled encrypted_key into the fixed-size jwe->cek buffer ++ if (recipient->enc_key.raw_len != jwe->cek_len + 8) ++ { ++ CJOSE_ERROR(err, CJOSE_ERR_INVALID_ARG); ++ return false; ++ } ++ + // AES unwrap the CEK in to jwe->cek + int len = AES_unwrap_key(&akey, (const unsigned char *)NULL, jwe->cek, (const unsigned char *)recipient->enc_key.raw, + recipient->enc_key.raw_len); +diff --git a/test/check_jwe.c b/test/check_jwe.c +--- a/test/check_jwe.c ++++ b/test/check_jwe.c +@@ -1024,6 +1024,84 @@ + } + END_TEST + ++// build a compact JWE from a valid AES-KW JWE but with the encrypted_key segment ++// replaced by an oversized (attacker-controlled) base64url blob, then confirm that ++// importing parses fine but decryption fails gracefully instead of overflowing the ++// fixed-size CEK buffer in AES_unwrap_key (RFC 3394 wrapped key is always cek_len + 8) ++static void _decrypt_oversized_aes_kw_ek(const char *alg, const char *enc, const char *key) ++{ ++ cjose_err err; ++ ++ cjose_jwk_t *jwk = cjose_jwk_import(key, strlen(key), &err); ++ ck_assert_msg(NULL != jwk, "cjose_jwk_import failed: %s", err.message); ++ ++ cjose_header_t *hdr = cjose_header_new(&err); ++ ck_assert(cjose_header_set(hdr, CJOSE_HDR_ALG, alg, &err)); ++ ck_assert(cjose_header_set(hdr, CJOSE_HDR_ENC, enc, &err)); ++ ++ const char *plain = "Setec Astronomy"; ++ cjose_jwe_t *jwe = cjose_jwe_encrypt(jwk, hdr, (const uint8_t *)plain, strlen(plain), &err); ++ ck_assert_msg(NULL != jwe, "cjose_jwe_encrypt failed: %s", err.message); ++ ++ char *compact = cjose_jwe_export(jwe, &err); ++ ck_assert_msg(NULL != compact, "cjose_jwe_export failed: %s", err.message); ++ ++ // locate the boundaries of the encrypted_key (second) segment ++ char *first_dot = strchr(compact, '.'); ++ ck_assert(NULL != first_dot); ++ char *second_dot = strchr(first_dot + 1, '.'); ++ ck_assert(NULL != second_dot); ++ ++ // craft an oversized encrypted_key: 1024 raw bytes base64url-encoded; this decodes ++ // to far more than any CEK length (16/24/32 ... 64) + 8 bytes ++ uint8_t oversized[1024]; ++ memset(oversized, 0x41, sizeof(oversized)); ++ char *oversized_b64u = NULL; ++ size_t oversized_b64u_len = 0; ++ ck_assert(cjose_base64url_encode(oversized, sizeof(oversized), &oversized_b64u, &oversized_b64u_len, &err)); ++ ++ // reassemble: <header>.<oversized_ek>.<iv>.<ciphertext>.<tag> ++ size_t header_len = first_dot - compact; ++ const char *tail = second_dot; // includes the leading '.' ++ size_t tail_len = strlen(tail); ++ size_t tampered_len = header_len + 1 + oversized_b64u_len + tail_len; ++ char *tampered = (char *)malloc(tampered_len + 1); ++ ck_assert(NULL != tampered); ++ memcpy(tampered, compact, header_len); ++ tampered[header_len] = '.'; ++ memcpy(tampered + header_len + 1, oversized_b64u, oversized_b64u_len); ++ memcpy(tampered + header_len + 1 + oversized_b64u_len, tail, tail_len); ++ tampered[tampered_len] = '\0'; ++ ++ // import must still succeed (parsing the oversized segment is legal) ++ cjose_jwe_t *jwe_bad = cjose_jwe_import(tampered, tampered_len, &err); ++ ck_assert_msg(NULL != jwe_bad, "cjose_jwe_import of oversized encrypted_key failed for alg %s: %s", alg, err.message); ++ ++ // decryption must fail cleanly (no heap overflow) with CJOSE_ERR_INVALID_ARG ++ size_t plain_len = 0; ++ uint8_t *decrypted = cjose_jwe_decrypt(jwe_bad, jwk, &plain_len, &err); ++ ck_assert_msg(NULL == decrypted, "cjose_jwe_decrypt succeeded on oversized encrypted_key for alg %s", alg); ++ ck_assert_msg(CJOSE_ERR_INVALID_ARG == err.code, "cjose_jwe_decrypt returned wrong err.code %d for alg %s", err.code, alg); ++ ++ free(tampered); ++ cjose_get_dealloc()(oversized_b64u); ++ cjose_get_dealloc()(compact); ++ cjose_jwe_release(jwe_bad); ++ cjose_jwe_release(jwe); ++ cjose_header_release(hdr); ++ cjose_jwk_release(jwk); ++} ++ ++START_TEST(test_cjose_jwe_decrypt_aes_kw_oversized_ek) ++{ ++ _decrypt_oversized_aes_kw_ek(CJOSE_HDR_ALG_A128KW, CJOSE_HDR_ENC_A128CBC_HS256, JWK_OCT_16); ++ _decrypt_oversized_aes_kw_ek(CJOSE_HDR_ALG_A192KW, CJOSE_HDR_ENC_A192CBC_HS384, JWK_OCT_24); ++ _decrypt_oversized_aes_kw_ek(CJOSE_HDR_ALG_A256KW, CJOSE_HDR_ENC_A256CBC_HS512, JWK_OCT_32); ++ _decrypt_oversized_aes_kw_ek(CJOSE_HDR_ALG_A128KW, CJOSE_HDR_ENC_A128GCM, JWK_OCT_16); ++ _decrypt_oversized_aes_kw_ek(CJOSE_HDR_ALG_A256KW, CJOSE_HDR_ENC_A256GCM, JWK_OCT_32); ++} ++END_TEST ++ + START_TEST(test_cjose_jwe_decrypt_rsa) + { + struct cjose_jwe_decrypt_rsa +@@ -1431,6 +1509,7 @@ + tcase_add_test(tc_jwe, test_cjose_jwe_self_encrypt_self_decrypt_many); + tcase_add_test(tc_jwe, test_cjose_jwe_decrypt_aes); + tcase_add_test(tc_jwe, test_cjose_jwe_decrypt_aes_gcm); ++ tcase_add_test(tc_jwe, test_cjose_jwe_decrypt_aes_kw_oversized_ek); + tcase_add_test(tc_jwe, test_cjose_jwe_decrypt_rsa); + tcase_add_test(tc_jwe, test_cjose_jwe_encrypt_with_bad_header); + tcase_add_test(tc_jwe, test_cjose_jwe_encrypt_with_bad_key); diff -Nru cjose-0.6.2.1/debian/patches/CVE-2026-53939.patch cjose-0.6.2.1/debian/patches/CVE-2026-53939.patch --- cjose-0.6.2.1/debian/patches/CVE-2026-53939.patch 1969-12-31 18:00:00.000000000 -0600 +++ cjose-0.6.2.1/debian/patches/CVE-2026-53939.patch 2026-11-02 11:45:04.000000000 -0600 @@ -0,0 +1,123 @@ +Origin: https://github.com/OpenIDC/cjose/commit/2a6e5bd969fa20059fb00913fb9f57d77ea6a4d9 +Reviewed-by: Simon Quigley <[email protected]> +Last-Update: 2026-11-02 + +Backported to 0.6.2.1. The suite function is cjose_jwe_suite(). + +From 2a6e5bd969fa20059fb00913fb9f57d77ea6a4d9 Mon Sep 17 00:00:00 2001 +From: Hans Zandbelt <[email protected]> +Date: Tue, 2 Jun 2026 14:44:33 +0200 +Subject: [PATCH] jwe: fix all-zero CEK for AES-CBC-HMAC content encryption + +_cjose_jwe_set_cek_aes_cbc() allocated the content-encryption key with +_cjose_jwe_malloc(keysize, !random, ...) -- the random flag was inverted +(the AES-GCM sibling correctly passes `random`). On the encryption path +random is true, so !random zero-filled the CEK instead of generating +random bytes. + +As a result every JWE produced with an AES-CBC-HMAC enc +(A128CBC-HS256 / A192CBC-HS384 / A256CBC-HS512) combined with a non-dir +key-management alg (A128/192/256KW, RSA-OAEP, RSA1_5) was encrypted and +MAC'd under an all-zero key -- a complete loss of confidentiality and +integrity for those ciphertexts. "dir" (key supplied by the JWK) and the +AES-GCM enc values were unaffected. + +Pass `random` so the CEK is generated from RAND_bytes, and correct the +now-inaccurate comment to match the GCM path. Add a regression test that +encrypts the same plaintext twice and asserts the encrypted_key differs +(it is byte-identical with a fixed zero CEK because AES key wrap is +deterministic). + +Signed-off-by: Hans Zandbelt <[email protected]> +Co-Authored-By: Claude Opus 4.8 <[email protected]> +--- +diff --git a/src/jwe.c b/src/jwe.c +--- a/src/jwe.c ++++ b/src/jwe.c +@@ -457,9 +457,9 @@ + + if (NULL == jwk) + { +- // allocate memory for the CEK and fill with random bytes or 0's ++ // if no JWK is provided, generate a random key + _cjose_release_cek(&jwe->cek, jwe->cek_len); +- if (!_cjose_jwe_malloc(keysize, !random, &jwe->cek, err)) ++ if (!_cjose_jwe_malloc(keysize, random, &jwe->cek, err)) + { + return false; + } +diff --git a/test/check_jwe.c b/test/check_jwe.c +--- a/test/check_jwe.c ++++ b/test/check_jwe.c +@@ -1494,6 +1494,63 @@ + } + END_TEST + ++// regression test for the AES-CBC-HMAC content-encryption key (CEK): it must ++// be generated from fresh random bytes on every encryption. A previous bug ++// zero-filled it (inverted random flag); because AES key wrap is deterministic, ++// that produced a byte-identical encrypted_key on every encryption with the ++// same KEK. Encrypt the same plaintext twice and assert the encrypted_key (the ++// 2nd compact field) differs. ++static void _assert_cbc_cek_random(const char *alg, const char *enc, const char *jwk_str) ++{ ++ cjose_err err; ++ ++ cjose_jwk_t *jwk = cjose_jwk_import(jwk_str, strlen(jwk_str), &err); ++ ck_assert_msg(NULL != jwk, "cjose_jwk_import failed: %s", err.message); ++ ++ cjose_header_t *hdr = cjose_header_new(&err); ++ ck_assert_msg(NULL != hdr, "cjose_header_new failed: %s", err.message); ++ ck_assert(cjose_header_set(hdr, CJOSE_HDR_ALG, alg, &err)); ++ ck_assert(cjose_header_set(hdr, CJOSE_HDR_ENC, enc, &err)); ++ ++ const char *plain = "Setec Astronomy"; ++ char ek[2][512]; ++ ++ for (int i = 0; i < 2; i++) ++ { ++ cjose_jwe_t *jwe = cjose_jwe_encrypt(jwk, hdr, (const uint8_t *)plain, strlen(plain), &err); ++ ck_assert_msg(NULL != jwe, "cjose_jwe_encrypt failed (%s/%s): %s", alg, enc, err.message); ++ ++ char *cser = cjose_jwe_export(jwe, &err); ++ ck_assert_msg(NULL != cser, "cjose_jwe_export failed: %s", err.message); ++ ++ // the encrypted_key is the 2nd of the 5 dot-separated compact parts ++ const char *d1 = strchr(cser, '.'); ++ const char *d2 = (NULL != d1) ? strchr(d1 + 1, '.') : NULL; ++ ck_assert_msg(NULL != d1 && NULL != d2, "malformed compact serialization"); ++ size_t n = (size_t)(d2 - (d1 + 1)); ++ ck_assert_msg(n > 0 && n < sizeof(ek[i]), "unexpected encrypted_key length"); ++ memcpy(ek[i], d1 + 1, n); ++ ek[i][n] = '\0'; ++ ++ cjose_get_dealloc()(cser); ++ cjose_jwe_release(jwe); ++ } ++ ++ ck_assert_msg(0 != strcmp(ek[0], ek[1]), ++ "AES-CBC-HMAC CEK is not random: identical encrypted_key across two encryptions (%s/%s)", alg, enc); ++ ++ cjose_header_release(hdr); ++ cjose_jwk_release(jwk); ++} ++ ++START_TEST(test_cjose_jwe_encrypt_cbc_cek_random) ++{ ++ _assert_cbc_cek_random(CJOSE_HDR_ALG_A128KW, CJOSE_HDR_ENC_A128CBC_HS256, JWK_OCT_16); ++ _assert_cbc_cek_random(CJOSE_HDR_ALG_A192KW, CJOSE_HDR_ENC_A192CBC_HS384, JWK_OCT_24); ++ _assert_cbc_cek_random(CJOSE_HDR_ALG_A256KW, CJOSE_HDR_ENC_A256CBC_HS512, JWK_OCT_32); ++} ++END_TEST ++ + Suite *cjose_jwe_suite() + { + Suite *suite = suite_create("jwe"); +@@ -1518,6 +1575,7 @@ + tcase_add_test(tc_jwe, test_cjose_jwe_import_invalid_serialization); + tcase_add_test(tc_jwe, test_cjose_jwe_decrypt_bad_params); + tcase_add_test(tc_jwe, test_cjose_jwe_multiple_recipients); ++ tcase_add_test(tc_jwe, test_cjose_jwe_encrypt_cbc_cek_random); + suite_add_tcase(suite, tc_jwe); + + return suite; diff -Nru cjose-0.6.2.1/debian/patches/series cjose-0.6.2.1/debian/patches/series --- cjose-0.6.2.1/debian/patches/series 2023-08-04 09:06:06.000000000 -0500 +++ cjose-0.6.2.1/debian/patches/series 2026-11-02 12:06:06.000000000 -0600 @@ -1 +1,3 @@ CVE-2023-37464.patch +CVE-2026-53938.patch +CVE-2026-53939.patch
OpenPGP_signature.asc
Description: OpenPGP digital signature
