CVE-2023-37464: Use of a Broken or Risky Cryptographic Algorithm (CWE-327)
value: OpenIDC/cjose is a C library implementing the Javascript Object Signing and Encryption (JOSE). The AES GCM decryption routine incorrectly uses the Tag length from the actual Authentication Tag provided in the JWE. The spec  says that a fixed length of 16 octets must be applied. Therefore this bug allows an attacker to provide a truncated Authentication Tag and to modify the JWE accordingly. Users should upgrade to a version >= 0.6.2.2. Users unable to upgrade should avoid using AES GCM encryption and replace it with another encryption algorithm (e.g. AES CBC).
CWE description: The product uses a broken or risky cryptographic algorithm or protocol.
CVSS v3 base score: 7.5
Published: 2023-07-14T21:15Z
Repository: https://github.com/OpenIDC/cjose
Language: C
Files changed: src/jwe.c, test/check_jwe.c
Commit date: 2023-07-12 10:52:45 +0200
Commit message: use fixed authentication tag length of 16 octets in AES GCM decryption

see: https://github.com/cisco/cjose/issues/125

Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
Diff: diff --git a/src/jwe.c b/src/jwe.c
index 48f0ac1..c3938e7 100644
--- a/src/jwe.c
+++ b/src/jwe.c
@@ -1284,10 +1284,16 @@ static bool _cjose_jwe_decrypt_dat_aes_gcm(cjose_jwe_t *jwe, cjose_err *err)
     {
         CJOSE_ERROR(err, CJOSE_ERR_CRYPTO);
         goto _cjose_jwe_decrypt_dat_aes_gcm_fail;
     }
 
+    if (jwe->enc_auth_tag.raw_len != 16)
+    {
+        CJOSE_ERROR(err, CJOSE_ERR_CRYPTO);
+        goto _cjose_jwe_decrypt_dat_aes_gcm_fail;
+    }
+
     // set the expected GCM-mode authentication tag
     if (EVP_CIPHER_CTX_ctrl(ctx, CJOSE_EVP_CTRL_GCM_SET_TAG, jwe->enc_auth_tag.raw_len, jwe->enc_auth_tag.raw) != 1)
     {
         CJOSE_ERROR(err, CJOSE_ERR_CRYPTO);
         goto _cjose_jwe_decrypt_dat_aes_gcm_fail;
diff --git a/test/check_jwe.c b/test/check_jwe.c
index 3d69a93..39621b1 100644
--- a/test/check_jwe.c
+++ b/test/check_jwe.c
@@ -965,10 +965,67 @@ START_TEST(test_cjose_jwe_decrypt_aes)
     cjose_jwe_release(jwe);
     cjose_jwk_release(jwk);
 }
 END_TEST
 
+START_TEST(test_cjose_jwe_decrypt_aes_gcm)
+{
+    cjose_err err;
+
+    const char *key = JWK_OCT_32;
+    const char *plain1 = "Live long and prosper.";
+    char *compact1 = "eyJhbGciOiAiZGlyIiwgImVuYyI6ICJBMjU2R0NNIn0..Du_9fxxV-zrReaWC.aS_rpokeuxkaPc2sykcQDCQuJCYoww.GpeKGEqd8KQ0v6JNea5aSA";
+    char *compact2 = "eyJhbGciOiAiZGlyIiwgImVuYyI6ICJBMjU2R0NNIn0..Du_9fxxV-zrReaWC.aS_rpokeuxkaPc2sykcQDCQuJCYoww.Gp";
+
+    cjose_jwk_t *jwk = cjose_jwk_import(key, strlen(key), &err);
+    ck_assert_msg(NULL != jwk,
+                  "cjose_jwk_import failed: "
+                  "%s, file: %s, function: %s, line: %ld",
+                  err.message, err.file, err.function, err.line);
+
+    cjose_jwe_t *jwe1 = cjose_jwe_import(compact1, strlen(compact1), &err);
+    ck_assert_msg(NULL != jwe1,
+                  "cjose_jwe_import failed: "
+                  "%s, file: %s, function: %s, line: %ld",
+                  err.message, err.file, err.function, err.line);
+
+    uint8_t *plain2 = NULL;
+    size_t plain2_len = 0;
+    plain2 = cjose_jwe_decrypt(jwe1, jwk, &plain2_len, &err);
+    ck_assert_msg(NULL != plain2,
+                  "cjose_jwe_decrypt failed: "
+                  "%s, file: %s, function: %s, line: %ld",
+                  err.message, err.file, err.function, err.line);
+
+    ck_assert_msg(plain2_len == strlen(plain1),
+                  "length of decrypted plaintext does not match length of original, "
+                  "expected: %lu, found: %lu",
+                  strlen(plain1), plain2_len);
+    ck_assert_msg(strncmp(plain1, plain2, plain2_len) == 0, "decrypted plaintext does not match encrypted plaintext");
+
+    cjose_get_dealloc()(plain2);
+    cjose_jwe_release(jwe1);
+
+    cjose_jwe_t *jwe2 = cjose_jwe_import(compact2, strlen(compact2), &err);
+    ck_assert_msg(NULL != jwe2,
+                   "cjose_jwe_import failed: "
+                   "%s, file: %s, function: %s, line: %ld",
+                   err.message, err.file, err.function, err.line);
+
+    uint8_t *plain3 = NULL;
+    size_t plain3_len = 0;
+    plain3 = cjose_jwe_decrypt(jwe2, jwk, &plain3_len, &err);
+    ck_assert_msg(NULL == plain3,
+                   "cjose_jwe_decrypt succeeded where it should have failed: "
+                   "%s, file: %s, function: %s, line: %ld",
+                   err.message, err.file, err.function, err.line);
+
+    cjose_jwe_release(jwe2);
+    cjose_jwk_release(jwk);
+}
+END_TEST
+
 START_TEST(test_cjose_jwe_decrypt_rsa)
 {
     struct cjose_jwe_decrypt_rsa
     {
         const char *jwe;
@@ -1371,10 +1428,11 @@ Suite *cjose_jwe_suite()
     tcase_add_test(tc_jwe, test_cjose_jwe_self_encrypt_self_decrypt_short);
     tcase_add_test(tc_jwe, test_cjose_jwe_self_encrypt_self_decrypt_empty);
     tcase_add_test(tc_jwe, test_cjose_jwe_self_encrypt_self_decrypt_large);
     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_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);
     tcase_add_test(tc_jwe, test_cjose_jwe_encrypt_with_bad_content);
     tcase_add_test(tc_jwe, test_cjose_jwe_import_export_compare);

Fixed code:     if (jwe->enc_auth_tag.raw_len != 16)
    {
        CJOSE_ERROR(err, CJOSE_ERR_CRYPTO);
        goto _cjose_jwe_decrypt_dat_aes_gcm_fail;
    }

START_TEST(test_cjose_jwe_decrypt_aes_gcm)
{
    cjose_err err;

    const char *key = JWK_OCT_32;
    const char *plain1 = "Live long and prosper.";
    char *compact1 = "eyJhbGciOiAiZGlyIiwgImVuYyI6ICJBMjU2R0NNIn0..Du_9fxxV-zrReaWC.aS_rpokeuxkaPc2sykcQDCQuJCYoww.GpeKGEqd8KQ0v6JNea5aSA";
    char *compact2 = "eyJhbGciOiAiZGlyIiwgImVuYyI6ICJBMjU2R0NNIn0..Du_9fxxV-zrReaWC.aS_rpokeuxkaPc2sykcQDCQuJCYoww.Gp";

    cjose_jwk_t *jwk = cjose_jwk_import(key, strlen(key), &err);
    ck_assert_msg(NULL != jwk,
                  "cjose_jwk_import failed: "
                  "%s, file: %s, function: %s, line: %ld",
                  err.message, err.file, err.function, err.line);

    cjose_jwe_t *jwe1 = cjose_jwe_import(compact1, strlen(compact1), &err);
    ck_assert_msg(NULL != jwe1,
                  "cjose_jwe_import failed: "
                  "%s, file: %s, function: %s, line: %ld",
                  err.message, err.file, err.function, err.line);

    uint8_t *plain2 = NULL;
    size_t plain2_len = 0;
    plain2 = cjose_jwe_decrypt(jwe1, jwk, &plain2_len, &err);
    ck_assert_msg(NULL != plain2,
                  "cjose_jwe_decrypt failed: "
                  "%s, file: %s, function: %s, line: %ld",
                  err.message, err.file, err.function, err.line);

    ck_assert_msg(plain2_len == strlen(plain1),
                  "length of decrypted plaintext does not match length of original, "
                  "expected: %lu, found: %lu",
                  strlen(plain1), plain2_len);
    ck_assert_msg(strncmp(plain1, plain2, plain2_len) == 0, "decrypted plaintext does not match encrypted plaintext");

    cjose_get_dealloc()(plain2);
    cjose_jwe_release(jwe1);

    cjose_jwe_t *jwe2 = cjose_jwe_import(compact2, strlen(compact2), &err);
    ck_assert_msg(NULL != jwe2,
                   "cjose_jwe_import failed: "
                   "%s, file: %s, function: %s, line: %ld",
                   err.message, err.file, err.function, err.line);

    uint8_t *plain3 = NULL;
    size_t plain3_len = 0;
    plain3 = cjose_jwe_decrypt(jwe2, jwk, &plain3_len, &err);
    ck_assert_msg(NULL == plain3,
                   "cjose_jwe_decrypt succeeded where it should have failed: "
                   "%s, file: %s, function: %s, line: %ld",
                   err.message, err.file, err.function, err.line);

    cjose_jwe_release(jwe2);
    cjose_jwk_release(jwk);
}
END_TEST

    tcase_add_test(tc_jwe, test_cjose_jwe_decrypt_aes_gcm);