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

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to