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 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 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/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 d867f21..92b68fe 100644 --- a/src/jws.c +++ b/src/jws.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -253,6 +254,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_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; } 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);