diff --git a/lib/hooks.h b/lib/hooks.h index d4cd5f93..4208c254 100644 --- a/lib/hooks.h +++ b/lib/hooks.h @@ -20,6 +20,8 @@ #include #include +#define MAX_COMPRESSED_SIZE (256*1024) + typedef enum { JOSE_HOOK_JWK_KIND_NONE = 0, JOSE_HOOK_JWK_KIND_TYPE, diff --git a/lib/jwe.c b/lib/jwe.c index 516245bd..55b43330 100644 --- a/lib/jwe.c +++ b/lib/jwe.c @@ -275,14 +275,8 @@ jose_jwe_enc_cek_io(jose_cfg_t *cfg, json_t *jwe, const json_t *cek, jose_io_t *next) { const jose_hook_alg_t *alg = NULL; - jose_io_auto_t *zip = NULL; - json_auto_t *prt = NULL; const char *h = NULL; const char *k = NULL; - const char *z = NULL; - - prt = jose_b64_dec_load(json_object_get(jwe, "protected")); - (void) json_unpack(prt, "{s:s}", "zip", &z); if (json_unpack(jwe, "{s?{s?s}}", "unprotected", "enc", &h) < 0) return NULL; @@ -336,19 +330,7 @@ jose_jwe_enc_cek_io(jose_cfg_t *cfg, json_t *jwe, const json_t *cek, if (!encode_protected(jwe)) return NULL; - if (z) { - const jose_hook_alg_t *a = NULL; - - a = jose_hook_alg_find(JOSE_HOOK_ALG_KIND_COMP, z); - if (!a) - return NULL; - - zip = a->comp.def(a, cfg, next); - if (!zip) - return NULL; - } - - return alg->encr.enc(alg, cfg, jwe, cek, zip ? zip : next); + return alg->encr.enc(alg, cfg, jwe, cek, next); } void * @@ -463,6 +445,12 @@ jose_jwe_dec_cek(jose_cfg_t *cfg, const json_t *jwe, const json_t *cek, o = jose_io_malloc(cfg, &pt, ptl); d = jose_jwe_dec_cek_io(cfg, jwe, cek, o); i = jose_b64_dec_io(d); + + /* Here we make sure the ciphertext is not larger than our + * compression limit. */ + if (zip_in_protected_header((json_t*)jwe) && ctl > MAX_COMPRESSED_SIZE) + return false; + if (!o || !d || !i || !i->feed(i, ct, ctl) || !i->done(i)) return NULL; diff --git a/lib/meson.build b/lib/meson.build index 86bb0fa9..28a4a4df 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -2,8 +2,14 @@ flags = '-Wl,--version-script=' + meson.current_source_dir() + '/libjose.map' code = 'int main() { return 0; }' cc = meson.get_compiler('c') -if not cc.links(code, args: flags, name: '-Wl,--version-script=...') - flags = [ '-export-symbols-regex=^jose_.*' ] +if host_machine.system() == 'freebsd' + if not cc.links(code, args: flags + ',--undefined-version' , name: '-Wl,--version-script=...') + flags = [ '-export-symbols-regex=^jose_.*' ] + endif +else + if not cc.links(code, args: flags, name: '-Wl,--version-script=...') + flags = [ '-export-symbols-regex=^jose_.*' ] + endif endif libjose_build_dir = meson.current_build_dir() diff --git a/lib/misc.c b/lib/misc.c index 465cd0d0..1015ce46 100644 --- a/lib/misc.c +++ b/lib/misc.c @@ -18,6 +18,7 @@ #include "misc.h" #include #include +#include "hooks.h" bool encode_protected(json_t *obj) @@ -42,6 +43,63 @@ zero(void *mem, size_t len) memset(mem, 0, len); } + +bool +handle_zip_enc(json_t *json, const void *in, size_t len, void **data, size_t *datalen) +{ + json_t *prt = NULL; + char *z = NULL; + const jose_hook_alg_t *a = NULL; + jose_io_auto_t *zip = NULL; + jose_io_auto_t *zipdata = NULL; + + prt = json_object_get(json, "protected"); + if (prt && json_is_string(prt)) + prt = jose_b64_dec_load(prt); + + /* Check if we have "zip" in the protected header. */ + if (json_unpack(prt, "{s:s}", "zip", &z) == -1) { + /* No zip. */ + *data = (void*)in; + *datalen = len; + return true; + } + + /* OK, we have "zip", so we should compress the payload before + * the encryption takes place. */ + a = jose_hook_alg_find(JOSE_HOOK_ALG_KIND_COMP, z); + if (!a) + return false; + + zipdata = jose_io_malloc(NULL, data, datalen); + if (!zipdata) + return false; + + zip = a->comp.def(a, NULL, zipdata); + if (!zip || !zip->feed(zip, in, len) || !zip->done(zip)) + return false; + + return true; +} + +bool +zip_in_protected_header(json_t *json) +{ + json_t *prt = NULL; + char *z = NULL; + + prt = json_object_get(json, "protected"); + if (prt && json_is_string(prt)) + prt = jose_b64_dec_load(prt); + + /* Check if we have "zip" in the protected header. */ + if (json_unpack(prt, "{s:s}", "zip", &z) == -1) + return false; + + /* We have "zip", but let's validate the alg also. */ + return jose_hook_alg_find(JOSE_HOOK_ALG_KIND_COMP, z) != NULL; +} + static void __attribute__((constructor)) constructor(void) { diff --git a/lib/misc.h b/lib/misc.h index d479d53f..18e7710a 100644 --- a/lib/misc.h +++ b/lib/misc.h @@ -30,3 +30,9 @@ encode_protected(json_t *obj); void zero(void *mem, size_t len); + +bool +handle_zip_enc(json_t *jwe, const void *in, size_t len, void **data, size_t *data_len); + +bool +zip_in_protected_header(json_t *jwe); diff --git a/lib/openssl/aescbch.c b/lib/openssl/aescbch.c index c8d5e605..00bd2ae1 100644 --- a/lib/openssl/aescbch.c +++ b/lib/openssl/aescbch.c @@ -18,6 +18,7 @@ #include "misc.h" #include #include "../hooks.h" +#include "../misc.h" #include #include @@ -155,9 +156,13 @@ enc_feed(jose_io_t *io, const void *in, size_t len) io_t *i = containerof(io, io_t, io); uint8_t ct[EVP_CIPHER_CTX_block_size(i->cctx) + 1]; - const uint8_t *pt = in; + uint8_t *pt = NULL; + size_t ptlen = 0; - for (size_t j = 0; j < len; j++) { + if (!handle_zip_enc(i->json, in, len, (void**)&pt, &ptlen)) + return false; + + for (size_t j = 0; j < ptlen; j++) { int l = 0; if (EVP_EncryptUpdate(i->cctx, ct, &l, &pt[j], 1) <= 0) diff --git a/lib/openssl/aesgcm.c b/lib/openssl/aesgcm.c index b4f55f2d..d3ab65cc 100644 --- a/lib/openssl/aesgcm.c +++ b/lib/openssl/aesgcm.c @@ -18,6 +18,7 @@ #include "misc.h" #include #include "../hooks.h" +#include "../misc.h" #include @@ -103,10 +104,15 @@ static bool enc_feed(jose_io_t *io, const void *in, size_t len) { io_t *i = containerof(io, io_t, io); - const uint8_t *pt = in; int l = 0; - for (size_t j = 0; j < len; j++) { + uint8_t *pt = NULL; + size_t ptlen = 0; + + if (!handle_zip_enc(i->json, in, len, (void**)&pt, &ptlen)) + return false; + + for (size_t j = 0; j < ptlen; j++) { uint8_t ct[EVP_CIPHER_CTX_block_size(i->cctx) + 1]; if (EVP_EncryptUpdate(i->cctx, ct, &l, &pt[j], 1) <= 0) diff --git a/lib/openssl/oct.c b/lib/openssl/oct.c index df4f0907..ef76b4ef 100644 --- a/lib/openssl/oct.c +++ b/lib/openssl/oct.c @@ -45,7 +45,7 @@ jwk_make_execute(jose_cfg_t *cfg, json_t *jwk) if (json_unpack(jwk, "{s:I}", "bytes", &len) < 0) return false; - if (len > KEYMAX) + if (len <= 0 || len > KEYMAX) return false; if (RAND_bytes(key, len) <= 0) diff --git a/lib/openssl/pbes2.c b/lib/openssl/pbes2.c index aae40059..1b3ca309 100644 --- a/lib/openssl/pbes2.c +++ b/lib/openssl/pbes2.c @@ -25,6 +25,8 @@ #include #define NAMES "PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW" +#define P2C_MIN_ITERATIONS 1000 +#define P2C_MAX_ITERATIONS 32768 static json_t * pbkdf2(const char *alg, jose_cfg_t *cfg, const json_t *jwk, int iter, @@ -193,7 +195,7 @@ alg_wrap_wrp(const jose_hook_alg_t *alg, jose_cfg_t *cfg, json_t *jwe, json_auto_t *hdr = NULL; const char *aes = NULL; json_t *h = NULL; - int p2c = 10000; + int p2c = P2C_MAX_ITERATIONS; size_t stl = 0; if (!json_object_get(cek, "k") && !jose_jwk_gen(cfg, cek)) @@ -226,7 +228,7 @@ alg_wrap_wrp(const jose_hook_alg_t *alg, jose_cfg_t *cfg, json_t *jwe, json_object_set_new(h, "p2c", json_integer(p2c)) < 0) return false; - if (p2c < 1000) + if (p2c < P2C_MIN_ITERATIONS || p2c > P2C_MAX_ITERATIONS) return false; if (json_object_set_new(h, "p2s", jose_b64_enc(st, stl)) == -1) @@ -268,6 +270,9 @@ alg_wrap_unw(const jose_hook_alg_t *alg, jose_cfg_t *cfg, const json_t *jwe, if (json_unpack(hdr, "{s:I}", "p2c", &p2c) == -1) return false; + if (p2c > P2C_MAX_ITERATIONS) + return false; + stl = jose_b64_dec(json_object_get(hdr, "p2s"), NULL, 0); if (stl < 8 || stl > sizeof(st)) return false; diff --git a/lib/zlib/deflate.c b/lib/zlib/deflate.c index 07eca0c9..04ded33e 100644 --- a/lib/zlib/deflate.c +++ b/lib/zlib/deflate.c @@ -113,6 +113,9 @@ def_free(jose_io_t *io) static bool inf_feed(jose_io_t *io, const void *in, size_t len) { + if (len > MAX_COMPRESSED_SIZE) { + return false; + } return feed(io, in, len, inflate); } diff --git a/meson.build b/meson.build index 436003cc..165268cb 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('jose', 'c', license: 'APL2', - version: '12', + version: '14', default_options: [ 'c_std=gnu99', 'prefix=/usr', diff --git a/tests/alg_comp.c b/tests/alg_comp.c index fcd305c1..33dc32e6 100644 --- a/tests/alg_comp.c +++ b/tests/alg_comp.c @@ -19,6 +19,10 @@ #include #include #include +#include + +static int g_high_compression_tested = 0; +static int g_low_compression_tested = 0; const struct { const char *alg; @@ -41,6 +45,63 @@ const struct { {} }; +const uint32_t long_string_tests[] = { + 2000, 200000, 10000000, 0 +}; + +static uint8_t* get_random_string(uint32_t length) +{ + assert(length); + uint8_t* c = (uint8_t*)malloc(length*sizeof(uint8_t)); + assert(c); + for (uint32_t i=0; icomp.def(a, NULL, b); + assert(z); + + assert(z->feed(z, str, inputlen)); + assert(z->done(z)); + + /* Test decompression now */ + c = jose_io_malloc(NULL, &buf2, &clen); + assert(b); + z = a->comp.inf(a, NULL, c); + assert(z); + + /* If length>MAX_COMPRESSED_SIZE, it must fail due to high decompression size */ + if(blen > MAX_COMPRESSED_SIZE) { + assert(!z->feed(z, buf1, blen)); + g_high_compression_tested = 1; + } else { + assert(z->feed(z, buf1, blen)); + g_low_compression_tested = 1; + /* Compare the final output with the original input. */ + assert(clen == inputlen); + assert(memcmp(buf2, str, inputlen) == 0); + } + assert(z->done(z)); + free(str); +} + static void test(const jose_hook_alg_t *a, bool iter, const uint8_t *i, size_t il) @@ -119,5 +180,12 @@ main(int argc, char *argv[]) tst_inf, sizeof(tst_inf)); } + for (size_t i = 0; long_string_tests[i]; i++) { + test_long_string(long_string_tests[i]); + } + + assert(1 == g_high_compression_tested); + assert(1 == g_low_compression_tested); + return EXIT_SUCCESS; } diff --git a/tests/api_jwe.c b/tests/api_jwe.c index f1d7a481..5fa4e100 100644 --- a/tests/api_jwe.c +++ b/tests/api_jwe.c @@ -19,8 +19,10 @@ #include #include +#include "../lib/hooks.h" /* for MAX_COMPRESSED_SIZE */ + static bool -dec(json_t *jwe, json_t *jwk) +dec_cmp(json_t *jwe, json_t *jwk, const char* expected_data, size_t expected_len) { bool ret = false; char *pt = NULL; @@ -30,10 +32,10 @@ dec(json_t *jwe, json_t *jwk) if (!pt) goto error; - if (ptl != 4) + if (ptl != expected_len) goto error; - if (strcmp(pt, "foo") != 0) + if (strcmp(pt, expected_data) != 0) goto error; ret = true; @@ -43,12 +45,40 @@ dec(json_t *jwe, json_t *jwk) return ret; } +static bool +dec(json_t *jwe, json_t *jwk) +{ + return dec_cmp(jwe, jwk, "foo", 4); +} + +struct zip_test_data_t { + char* data; + size_t datalen; + bool expected; +}; + +static char* +make_data(size_t len) +{ + assert(len > 0); + + char *data = malloc(len); + assert(data); + + for (size_t i = 0; i < len; i++) { + data[i] = 'A' + (random() % 26); + } + data[len-1] = '\0'; + return data; +} + int main(int argc, char *argv[]) { json_auto_t *jwke = json_pack("{s:s}", "alg", "ECDH-ES+A128KW"); json_auto_t *jwkr = json_pack("{s:s}", "alg", "RSA1_5"); json_auto_t *jwko = json_pack("{s:s}", "alg", "A128KW"); + json_auto_t *jwkz = json_pack("{s:s, s:i}", "kty", "oct", "bytes", 16); json_auto_t *set0 = json_pack("{s:[O,O]}", "keys", jwke, jwko); json_auto_t *set1 = json_pack("{s:[O,O]}", "keys", jwkr, jwko); json_auto_t *set2 = json_pack("{s:[O,O]}", "keys", jwke, jwkr); @@ -57,6 +87,7 @@ main(int argc, char *argv[]) assert(jose_jwk_gen(NULL, jwke)); assert(jose_jwk_gen(NULL, jwkr)); assert(jose_jwk_gen(NULL, jwko)); + assert(jose_jwk_gen(NULL, jwkz)); json_decref(jwe); assert((jwe = json_object())); @@ -98,5 +129,67 @@ main(int argc, char *argv[]) assert(dec(jwe, set1)); assert(dec(jwe, set2)); + + json_decref(jwe); + assert((jwe = json_pack("{s:{s:s,s:s,s:s,s:s}}", "protected", "alg", "A128KW", "enc", "A128GCM", "typ", "JWE", "zip", "DEF"))); + assert(jose_jwe_enc(NULL, jwe, NULL, jwkz, "foo", 4)); + assert(dec(jwe, jwkz)); + assert(!dec(jwe, jwkr)); + assert(!dec(jwe, jwko)); + assert(!dec(jwe, set0)); + assert(!dec(jwe, set1)); + assert(!dec(jwe, set2)); + + /* Some tests with "zip": "DEF" */ + struct zip_test_data_t zip[] = { + { + .data = make_data(5), + .datalen = 5, + .expected = true, + }, + { + .data = make_data(50), + .datalen = 50, + .expected = true, + }, + { + .data = make_data(1000), + .datalen = 1000, + .expected = true, + }, + { + .data = make_data(10000000), + .datalen = 10000000, + .expected = false, /* compressed len will be ~8000000+ + * (i.e. > MAX_COMPRESSED_SIZE) + */ + }, + { + .data = make_data(50000), + .datalen = 50000, + .expected = true + }, + { + + .data = NULL + } + }; + + for (size_t i = 0; zip[i].data != NULL; i++) { + json_decref(jwe); + assert((jwe = json_pack("{s:{s:s,s:s,s:s,s:s}}", "protected", "alg", "A128KW", "enc", "A128GCM", "typ", "JWE", "zip", "DEF"))); + assert(jose_jwe_enc(NULL, jwe, NULL, jwkz, zip[i].data, zip[i].datalen)); + + /* Now let's get the ciphertext compressed len. */ + char *ct = NULL; + size_t ctl = 0; + assert(json_unpack(jwe, "{s:s%}", "ciphertext", &ct, &ctl) != -1); + /* And check our expectation is correct. */ + assert(zip[i].expected == (ctl < MAX_COMPRESSED_SIZE)); + + assert(dec_cmp(jwe, jwkz, zip[i].data, zip[i].datalen) == zip[i].expected); + free(zip[i].data); + zip[i].data = NULL; + } return EXIT_SUCCESS; } diff --git a/tests/cve-2023-50967/cve-2023-50967.jwe b/tests/cve-2023-50967/cve-2023-50967.jwe new file mode 100644 index 00000000..70bfc42e --- /dev/null +++ b/tests/cve-2023-50967/cve-2023-50967.jwe @@ -0,0 +1 @@ +{"ciphertext":"aaPb-JYGACs-loPwJkZewg","encrypted_key":"P1h8q8wLVxqYsZUuw6iEQTzgXVZHCsu8Eik-oqbE4AJGIDto3gb3SA","header":{"alg":"PBES2-HS256+A128KW","p2c":1000000000,"p2s":"qUQQWWkyyIqculSiC93mlg"},"iv":"Clg3JX9oNl_ck3sLSGrlgg","protected":"eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0","tag":"i7vga9tJkwRswFd7HlyD_A"} diff --git a/tests/cve-2023-50967/cve-2023-50967.jwk b/tests/cve-2023-50967/cve-2023-50967.jwk new file mode 100644 index 00000000..d7fb1beb --- /dev/null +++ b/tests/cve-2023-50967/cve-2023-50967.jwk @@ -0,0 +1 @@ +{"alg":"PBES2-HS256+A128KW","k":"VHBLJ4-PmnqELoKbQoXuRA","key_ops":["wrapKey","unwrapKey"],"kty":"oct"} diff --git a/tests/jose-jwe-dec b/tests/jose-jwe-dec index 7a0c3bfb..e02d12e0 100755 --- a/tests/jose-jwe-dec +++ b/tests/jose-jwe-dec @@ -53,3 +53,8 @@ test "`jose jwe dec -i $prfx.12.jweg -k $prfx.12.jwk`" = "`cat $prfx.12.pt`" test "`jose jwe dec -i $prfx.13.jweg -k $prfx.13.1.jwk`" = "`cat $prfx.13.pt`" test "`jose jwe dec -i $prfx.13.jweg -k $prfx.13.2.jwk`" = "`cat $prfx.13.pt`" test "`jose jwe dec -i $prfx.13.jweg -k $prfx.13.3.jwk`" = "`cat $prfx.13.pt`" + +# CVE-2023-50967 - test originally from https://github.com/P3ngu1nW/CVE_Request/blob/main/latch-jose.md +# This test is expected to fail quickly on patched systems. +prfx="${CVE_2023_50967}/cve-2023-50967" +! test "$(jose jwe dec -i $prfx.jwe -k $prfx.jwk)" diff --git a/tests/jose-jwe-enc b/tests/jose-jwe-enc index 5391fd8d..d9969ece 100755 --- a/tests/jose-jwe-enc +++ b/tests/jose-jwe-enc @@ -74,4 +74,13 @@ for msg in "hi" "this is a longer message that is more than one block"; do printf '%s' "$msg" | jose jwe enc -I- -k $jwk -o $jwe [ "`jose jwe dec -i $jwe -k $jwk -O-`" = "$msg" ] done + + # "zip": "DEF" + tmpl='{"kty":"oct","bytes":32}' + for enc in A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM; do + jose jwk gen -i "${tmpl}" -o "${jwk}" + zip="$(printf '{"alg":"A128KW","enc":"%s","zip":"DEF"}' "${enc}")" + printf '%s' "${msg}" | jose jwe enc -i "${zip}" -I- -k "${jwk}" -o "${jwe}" + [ "$(jose jwe dec -i "${jwe}" -k "${jwk}" -O-)" = "${msg}" ] + done done diff --git a/tests/meson.build b/tests/meson.build index 37b910a8..1de53a13 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -31,6 +31,8 @@ progs = [ e = environment() e.prepend('PATH', meson.current_build_dir() + '/../cmd', separator: ':') e.set('VECTORS', meson.current_source_dir() + '/vectors') +e.set('CVE_2023_50967', meson.current_source_dir() + '/cve-2023-50967') + foreach p: progs exe = executable(p, p + '.c', dependencies: libjose_dep)