From 2c4ba3b395c4a62b880d34c704c5bce9974dc7f0 Mon Sep 17 00:00:00 2001 From: Hans Zandbelt Date: Fri, 11 Sep 2026 11:26:42 +0200 Subject: [PATCH 1/4] jws: check the key type before the HMAC digest step (#168) _cjose_jws_build_dig_hmac_sha() handed jwk->keydata to HMAC_Init_ex() as keysize/8 octets of key material for any key type; the key type was only checked afterwards in _cjose_jws_build_sig_hmac_sha(). For an EC or RSA key keydata is a (much smaller) key structure, so cjose_jws_sign() with an HS256/HS384/HS512 header and such a key computed an HMAC over the structure and past the end of its allocation before failing; valgrind flags the over-read. The call did return CJOSE_ERR_INVALID_ARG, so the visible behaviour does not change. Reject a non-oct key up front, as the other digest and signing steps do for their key types. The regression test signs with EC and RSA keys under the three HS algorithms; it fails on master under valgrind. Signed-off-by: Hans Zandbelt Co-authored-by: Claude Fable 5.1 --- CHANGELOG.md | 7 +++++++ src/jws.c | 7 +++++++ test/check_jws.c | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa46d7..04d8e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Release Notes # + +## [Unreleased](https://github.com/cisco/cjose/0.7.0..master) + +### Fix + +* Check the key type before the HMAC digest step: cjose_jws_sign with an HS256, HS384 or HS512 header and an EC or RSA key ran the HMAC over the key structure and past the end of its allocation before failing with CJOSE_ERR_INVALID_ARG + ## [0.7.0](https://github.com/cisco/cjose/0.6.3..0.7.0) (2026-09-09) diff --git a/src/jws.c b/src/jws.c index d867f21..76496c7 100644 --- a/src/jws.c +++ b/src/jws.c @@ -253,6 +253,13 @@ static bool _cjose_jws_build_dig_hmac_sha(cjose_jws_t *jws, const cjose_jwk_t *j bool retval = false; HMAC_CTX *ctx = NULL; + // ensure jwk is OCT: only then is keydata the raw key material + if (jwk->kty != CJOSE_JWK_KTY_OCT) + { + CJOSE_ERROR(err, CJOSE_ERR_INVALID_ARG); + return false; + } + // make sure we have an alg header json_t *alg_obj = json_object_get(jws->hdr, CJOSE_HDR_ALG); if (NULL == alg_obj) diff --git a/test/check_jws.c b/test/check_jws.c index 44f9d95..8d1df0a 100644 --- a/test/check_jws.c +++ b/test/check_jws.c @@ -392,6 +392,43 @@ START_TEST(test_cjose_jws_sign_with_bad_key) } END_TEST +START_TEST(test_cjose_jws_sign_hmac_with_non_oct_key) +{ + cjose_err err; + static const uint8_t plain[] = "The mind is everything. What you think you become."; + size_t plain_len = sizeof(plain) - 1; + + // HS256/HS384/HS512 take an oct key; for any other key type keydata is a + // key structure, not raw key material, and must not be fed to the HMAC. + // Before the fix the digest step ran the HMAC over keysize/8 octets of + // jwk->keydata and only the signing step rejected the key type: an + // over-read of the (much smaller) EC or RSA key structure that valgrind + // flags. The P-521 key reads 65 octets from a 16 octet allocation. + cjose_jwk_t *keys[3] = { NULL, NULL, NULL }; + keys[0] = cjose_jwk_import(JWK_COMMON_EC, strlen(JWK_COMMON_EC), &err); + keys[1] = cjose_jwk_create_EC_random(CJOSE_JWK_EC_P_521, &err); + keys[2] = cjose_jwk_import(JWK_COMMON, strlen(JWK_COMMON), &err); // RSA + static const char *ALGS[] = { CJOSE_HDR_ALG_HS256, CJOSE_HDR_ALG_HS384, CJOSE_HDR_ALG_HS512 }; + + cjose_header_t *hdr = cjose_header_new(&err); + ck_assert(NULL != hdr); + for (size_t k = 0; k < sizeof(keys) / sizeof(keys[0]); k++) + { + ck_assert_msg(NULL != keys[k], "creating key %zu failed: %s", k, err.message); + for (size_t a = 0; a < sizeof(ALGS) / sizeof(ALGS[0]); a++) + { + ck_assert(cjose_header_set(hdr, CJOSE_HDR_ALG, ALGS[a], &err)); + cjose_jws_t *jws = cjose_jws_sign(keys[k], hdr, plain, plain_len, &err); + ck_assert_msg(NULL == jws, "cjose_jws_sign [%s] accepted a non-oct key (key %zu)", ALGS[a], k); + ck_assert_msg(err.code == CJOSE_ERR_INVALID_ARG, "cjose_jws_sign [%s] returned wrong err.code (%u:%s)", ALGS[a], + err.code, err.message); + } + cjose_jwk_release(keys[k]); + } + cjose_header_release(hdr); +} +END_TEST + START_TEST(test_cjose_jws_sign_with_bad_content) { cjose_err err; @@ -1199,6 +1236,7 @@ Suite *cjose_jws_suite(void) tcase_add_test(tc_jws, test_cjose_jws_es256k_rejects_wrong_curve); tcase_add_test(tc_jws, test_cjose_jws_sign_with_bad_header); tcase_add_test(tc_jws, test_cjose_jws_sign_with_bad_key); + tcase_add_test(tc_jws, test_cjose_jws_sign_hmac_with_non_oct_key); tcase_add_test(tc_jws, test_cjose_jws_sign_with_bad_content); tcase_add_test(tc_jws, test_cjose_jws_import_export_compare); tcase_add_test(tc_jws, test_cjose_jws_import_invalid_serialization); From 963e036b0caa48557a2a77a034be15604800d27e Mon Sep 17 00:00:00 2001 From: Hans Zandbelt Date: Fri, 11 Sep 2026 12:39:11 +0200 Subject: [PATCH 2/4] Restore the build against OpenSSL 1.1.0 and 1.1.1 The build sets OPENSSL_API_COMPAT=0x10100000L. Under that level OpenSSL 1.1.x no longer provides the pre-1.1.0 compatibility names EVP_CIPHER_CTX_init, ERR_load_crypto_strings, OpenSSL_add_all_algorithms, EVP_cleanup and ERR_free_strings, which 1.0.x implements as functions and 3.x still defines as macros, so the library and the test harness failed to compile with implicit function declarations. jws.c also relied on a transitive include that went away when the public headers stopped including OpenSSL headers (#166); 1.0.x and 3.x happen to pull it in through . Drop the EVP_CIPHER_CTX_init calls, which are redundant right after EVP_CIPHER_CTX_new on every release, include bn.h where the BIGNUM functions are used, and confine the explicit initialisation and cleanup in the test harness to OpenSSL before 1.1.0, which initialises itself. README.md promises OpenSSL 1.0.1h or newer; the OpenSSL workflow that follows builds against the last release of every series to keep it that way. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Hans Zandbelt --- src/jwe.c | 4 ---- src/jws.c | 1 + test/check_cjose.c | 9 +++++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/jwe.c b/src/jwe.c index 84d535f..d6b6347 100644 --- a/src/jwe.c +++ b/src/jwe.c @@ -1354,7 +1354,6 @@ static bool _cjose_jwe_encrypt_dat_aes_gcm(cjose_jwe_t *jwe, const uint8_t *plai CJOSE_ERROR(err, CJOSE_ERR_CRYPTO); goto _cjose_jwe_encrypt_dat_fail; } - EVP_CIPHER_CTX_init(ctx); // initialize context for encryption using the AES GCM cipher and CEK and IV if (EVP_EncryptInit_ex(ctx, cipher, NULL, jwe->cek, jwe->enc_iv.raw) != 1) @@ -1558,7 +1557,6 @@ static bool _cjose_jwe_encrypt_dat_aes_cbc(cjose_jwe_t *jwe, const uint8_t *plai CJOSE_ERROR(err, CJOSE_ERR_CRYPTO); goto _cjose_jwe_encrypt_dat_aes_cbc_fail; } - EVP_CIPHER_CTX_init(ctx); // initialize context for decryption using the cipher, the 2nd half of the CEK and the IV if (EVP_EncryptInit_ex(ctx, cipher, NULL, jwe->cek + jwe->cek_len / 2, jwe->enc_iv.raw) != 1) @@ -1664,7 +1662,6 @@ 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; } - EVP_CIPHER_CTX_init(ctx); if (jwe->enc_iv.raw_len != 12) { @@ -1798,7 +1795,6 @@ static bool _cjose_jwe_decrypt_dat_aes_cbc(cjose_jwe_t *jwe, cjose_err *err) CJOSE_ERROR(err, CJOSE_ERR_CRYPTO); goto _cjose_jwe_decrypt_dat_aes_cbc_fail; } - EVP_CIPHER_CTX_init(ctx); // initialize context for decryption using the cipher, the 2nd half of the CEK and the IV if (EVP_DecryptInit_ex(ctx, cipher, NULL, jwe->cek + jwe->cek_len / 2, jwe->enc_iv.raw) != 1) diff --git a/src/jws.c b/src/jws.c index 76496c7..92b68fe 100644 --- a/src/jws.c +++ b/src/jws.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include diff --git a/test/check_cjose.c b/test/check_cjose.c index 4861336..b58acc9 100644 --- a/test/check_cjose.c +++ b/test/check_cjose.c @@ -7,6 +7,7 @@ #include #include #include +#include Suite *cjose_suite(void) { @@ -17,9 +18,11 @@ Suite *cjose_suite(void) int main(void) { - // initialize "OpenSSL" crypto + // initialize "OpenSSL" crypto (automatic since OpenSSL 1.1.0) +#if OPENSSL_VERSION_NUMBER < 0x10100000L ERR_load_crypto_strings(); OpenSSL_add_all_algorithms(); +#endif // setup suites SRunner *runner = srunner_create(cjose_suite()); @@ -39,9 +42,11 @@ int main(void) int failed = srunner_ntests_failed(runner); srunner_free(runner); - // cleanup "OpenSSL" crypto + // cleanup "OpenSSL" crypto (automatic since OpenSSL 1.1.0) +#if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_cleanup(); ERR_free_strings(); +#endif return (0 == failed) ? EXIT_SUCCESS : EXIT_FAILURE; } From f9a7d371107bae6576bdb19574e2a46806b1fd3d Mon Sep 17 00:00:00 2001 From: Hans Zandbelt Date: Fri, 11 Sep 2026 12:39:11 +0200 Subject: [PATCH 3/4] ci: test against the last release of every OpenSSL series The Build and Archs workflows test against the OpenSSL that the platform packages provide, which is a recent release, so behaviour that only differs in another release goes unnoticed, and README.md promises OpenSSL 1.0.1h or newer. Add an OpenSSL workflow that builds the last release of every series from 1.0.1 to 4.0 from the GitHub release tarball (sha256 pinned, cached across runs), configures cjose with OPENSSL_ROOT_DIR pointing at it, checks with ldd that check_cjose loads that library rather than the system one, and runs the tests. 3.0.7 is in the matrix as well: it is the release RHEL 9 is based on and the last 3.0.x whose EdDSA signing dereferences a missing private key instead of failing; 3.0.8 added the guard. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Hans Zandbelt --- .github/workflows/openssl.yml | 89 +++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/openssl.yml diff --git a/.github/workflows/openssl.yml b/.github/workflows/openssl.yml new file mode 100644 index 0000000..fa03170 --- /dev/null +++ b/.github/workflows/openssl.yml @@ -0,0 +1,89 @@ +name: OpenSSL + +on: + pull_request: + push: + branches: [master] + +jobs: + format: + uses: ./.github/workflows/format.yml + + build: + needs: format + runs-on: ubuntu-latest + name: OpenSSL ${{ matrix.openssl }} + + # The Build and Archs workflows test against the OpenSSL that the platform + # packages provide, which is a recent release. README.md promises OpenSSL + # 1.0.1h or newer, so this workflow builds the last release of every + # release series from source and tests against it, plus 3.0.7: the release + # RHEL 9 is based on and the last 3.0.x whose EdDSA signing dereferences a + # missing private key instead of failing (guarded in 3.0.8). + strategy: + fail-fast: false + matrix: + include: + - { openssl: 1.0.1u, tag: OpenSSL_1_0_1u, sha256: 4312b4ca1215b6f2c97007503d80db80d5157f76f8f7d3febbe6b4c56ff26739 } + - { openssl: 1.0.2u, tag: OpenSSL_1_0_2u, sha256: ecd0c6ffb493dd06707d38b14bb4d8c2288bb7033735606569d8f90f89669d16 } + - { openssl: 1.1.0l, tag: OpenSSL_1_1_0l, sha256: 74a2f756c64fd7386a29184dc0344f4831192d61dc2481a93a4c5dd727f41148 } + - { openssl: 1.1.1w, tag: OpenSSL_1_1_1w, sha256: cf3098950cb4d853ad95c0841f1f9c6d3dc102dccfcacd521d93925208b76ac8 } + - { openssl: 3.0.7, tag: openssl-3.0.7, sha256: 83049d042a260e696f62406ac5c08bf706fd84383f945cf21bd61e9ed95c396e } + - { openssl: 3.0.22, tag: openssl-3.0.22, sha256: 67ebca7e50d17383028045486653492195b83db95f8558709701bb47b5c1ef81 } + - { openssl: 3.1.8, tag: openssl-3.1.8, sha256: d319da6aecde3aa6f426b44bbf997406d95275c5c59ab6f6ef53caaa079f456f } + - { openssl: 3.2.6, tag: openssl-3.2.6, sha256: 89681a9ddaa9ed7cf25ea8ef61338db805200bae47d00510490623547380c148 } + - { openssl: 3.3.7, tag: openssl-3.3.7, sha256: 4900be54e81c4dfe00bb1a10dad33fd8414833573c40d0e9e3274d4ed32e53a2 } + - { openssl: 3.4.7, tag: openssl-3.4.7, sha256: 7261c348e6e59bc7b635a30f98072e8817a1c60a7033bb570737394a3a061842 } + - { openssl: 3.5.8, tag: openssl-3.5.8, sha256: a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2 } + - { openssl: 3.6.4, tag: openssl-3.6.4, sha256: 9bffaa1ad1e07b354c21bd3324ec02fa15579f45a7d0494b3e74bc449b7333ef } + - { openssl: 4.0.2, tag: openssl-4.0.2, sha256: 736b467530f916737b7031310ccb21d8218c6229e61e8e160cd1d3458cd543a8 } + + steps: + - uses: actions/checkout@v5 + - name: Dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y cmake pkg-config gcc libjansson-dev check + echo "OPENSSL_PREFIX=$HOME/openssl-${{ matrix.openssl }}" >> "$GITHUB_ENV" + - name: Cache OpenSSL ${{ matrix.openssl }} + id: cache-openssl + uses: actions/cache@v6 + with: + path: ~/openssl-${{ matrix.openssl }} + key: openssl-${{ matrix.openssl }}-${{ runner.os }}-${{ runner.arch }} + - name: Build OpenSSL ${{ matrix.openssl }} + if: steps.cache-openssl.outputs.cache-hit != 'true' + working-directory: ${{ runner.temp }} + run: | + curl -fsSLO "https://github.com/openssl/openssl/releases/download/${{ matrix.tag }}/openssl-${{ matrix.openssl }}.tar.gz" + echo "${{ matrix.sha256 }} openssl-${{ matrix.openssl }}.tar.gz" | sha256sum -c - + tar xzf "openssl-${{ matrix.openssl }}.tar.gz" + cd "openssl-${{ matrix.openssl }}" + case "${{ matrix.openssl }}" in + 1.0.*) + # static by default, no --libdir, needs 'make depend', parallel make is unreliable + ./config --prefix="$OPENSSL_PREFIX" shared + make depend + make + ;; + 1.1.0*) + # has --libdir but not no-tests + ./config --prefix="$OPENSSL_PREFIX" --libdir=lib + make -j"$(nproc)" + ;; + *) + ./config --prefix="$OPENSSL_PREFIX" --libdir=lib no-tests + make -j"$(nproc)" + ;; + esac + make install_sw + - name: Configure + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DOPENSSL_ROOT_DIR="$OPENSSL_PREFIX" + - name: Build + run: cmake --build build --parallel + - name: Check which OpenSSL the tests load + run: | + LD_LIBRARY_PATH="$OPENSSL_PREFIX/lib" "$OPENSSL_PREFIX/bin/openssl" version + ldd build/check_cjose | grep -F "$OPENSSL_PREFIX/lib/libcrypto" + - name: Test + run: ctest --test-dir build --output-on-failure From c7152286c10ae6380318ea70a6d6fcbc3541cc5f Mon Sep 17 00:00:00 2001 From: Hans Zandbelt Date: Fri, 11 Sep 2026 12:46:39 +0200 Subject: [PATCH 4/4] ci: move to actions/cache@v6 actions/cache@v4 targets Node.js 20, which GitHub has deprecated on its runners: every run carries a "Node.js 20 is deprecated ... forced to run on Node.js 24: actions/cache@v4" annotation. v5 moved the action to Node.js 24 with unchanged inputs and v6 is the current major. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Hans Zandbelt --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5b23e8..3cc2579 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Cache vcpkg binary archives - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ${{ env.LOCALAPPDATA }}\vcpkg\archives key: vcpkg-archives-${{ runner.os }}-x64-windows-openssl-jansson-check