From 3367b44a393743400a9ce92b6b714aa0791a80b0 Mon Sep 17 00:00:00 2001 From: "Brian J. France" Date: Thu, 16 Jul 2026 18:42:13 +0000 Subject: [PATCH] skip protected-header JSON parse when "zip" is absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `handle_zip_enc()` and `zip_in_protected_header()` sit on the JWE encrypt hot path. For every call they base64url-decode the `protected` header and run a full JSON parse (`json_loadb`) just to check for a `zip` key. This gates the expensive JSON parse behind a cheap substring scan: decode the header, scan the **decoded** bytes for the literal `"zip"`, and only parse when it's present. When `zip` is absent, we skip the parse and take the existing no-zip path. A false-positive substring just falls through to the real parse, so behaviour is unchanged in every case. An earlier version of this optimization scanned the base64url-**encoded** `protected` string for `"zip"`. That is incorrect: a `zip` key does not survive base64url encoding as the literal substring `"zip"` — it lands on shifting bit boundaries (e.g. `{"...","zip":"DEF"}` encodes with `zip` appearing as `emlw`). Scanning the encoded form yields false negatives, so compression is silently skipped on zip-enabled messages and the JWE fails to round-trip. This version scans the decoded JSON text, where a `zip` key must appear verbatim. `zip_in_protected_header` on a ~56-byte header, `-O2`, best of 3: | Header | Before | After | Delta | |-------------|----------|-----------|------------------| | `zip` absent | ~4.5 µs | ~3.2 µs | ~30% faster (parse skipped) | | `zip` present | ~6.0 µs | ~6.0 µs | within noise (+~21 ns memmem) | The added cost on the (rare) zip-enabled path is a single `memmem` over the decoded header (~21 ns); the win on the common no-zip path is the eliminated `json_loadb`. - `meson test` — **all 26 tests pass**, including `api_jwe`'s zip compress/decompress round-trip (which the encoded-scan version failed with SIGABRT). - Verified the clean base passes the same suite, so no pre-existing failures. - The pre-check requires `_GNU_SOURCE` for `memmem` and `` for the `jose_calloc`/`jose_free` allocators (used to mirror `jose_b64_dec_load`'s scrub-and-free of the decoded buffer). - `prt` is a borrowed reference from `json_object_get()` at the early-return points; it's cleared to `NULL` before returning so the `json_auto_t` cleanup doesn't spuriously `json_decref` it. --- lib/misc.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/lib/misc.c b/lib/misc.c index f9cacd9..c8707ee 100644 --- a/lib/misc.c +++ b/lib/misc.c @@ -15,8 +15,10 @@ * limitations under the License. */ +#define _GNU_SOURCE #include "misc.h" #include +#include #include #include "hooks.h" @@ -43,6 +45,51 @@ zero(void *mem, size_t len) memset(mem, 0, len); } +/* Decode the base64url-encoded protected header and load it as JSON only + * when it can contain a "zip" key. The full JSON parse is the expensive + * step (the base64url decode is comparatively cheap), so we gate it behind + * a substring scan for "zip" over the *decoded* bytes: a "zip" key is only + * possible if the literal bytes "zip" appear in the decoded JSON text. + * + * The scan must run on the decoded bytes, NOT on the base64url-encoded + * string: a "zip" key does not survive base64url encoding as the literal + * substring "zip", so scanning the encoded form yields false negatives. + * + * When "zip" is definitely absent, *no_zip is set to true and NULL is + * returned without parsing. On decode error, *no_zip stays false and NULL + * is returned (callers then treat it the same as a header with no zip). */ +static json_t * +load_protected_check_zip(const json_t *prt, bool *no_zip) +{ + uint8_t *buf = NULL; + json_t *out = NULL; + size_t size = 0; + + *no_zip = false; + + size = jose_b64_dec(prt, NULL, 0); + if (size == SIZE_MAX) + return NULL; + + buf = jose_calloc(1, size); + if (!buf) + return NULL; + + if (jose_b64_dec(prt, buf, size) != size) { + zero(buf, size); + jose_free(buf); + return NULL; + } + + if (memmem(buf, size, "zip", 3)) + out = json_loadb((char *) buf, size, JSON_DECODE_ANY, NULL); + else + *no_zip = true; + + zero(buf, size); + jose_free(buf); + return out; +} bool handle_zip_enc(json_t *json, const void *in, size_t len, void **data, size_t *datalen) @@ -54,8 +101,19 @@ handle_zip_enc(json_t *json, const void *in, size_t len, void **data, size_t *da jose_io_auto_t *zipdata = NULL; prt = json_object_get(json, "protected"); - if (prt && json_is_string(prt)) - prt = jose_b64_dec_load(prt); + if (prt && json_is_string(prt)) { + bool no_zip = false; + json_t *loaded = load_protected_check_zip(prt, &no_zip); + if (no_zip) { + /* No zip; skip the full parse. (prt is a borrowed reference, + * so clear it before returning to avoid a spurious decref.) */ + prt = NULL; + *data = (void*)in; + *datalen = len; + return true; + } + prt = loaded; + } /* Check if we have "zip" in the protected header. */ if (json_unpack(prt, "{s:s}", "zip", &z) == -1) { @@ -89,8 +147,17 @@ zip_in_protected_header(json_t *json) char *z = NULL; prt = json_object_get(json, "protected"); - if (prt && json_is_string(prt)) - prt = jose_b64_dec_load(prt); + if (prt && json_is_string(prt)) { + bool no_zip = false; + json_t *loaded = load_protected_check_zip(prt, &no_zip); + if (no_zip) { + /* No zip; skip the full parse. (prt is a borrowed reference, + * so clear it before returning to avoid a spurious decref.) */ + prt = NULL; + return false; + } + prt = loaded; + } /* Check if we have "zip" in the protected header. */ if (json_unpack(prt, "{s:s}", "zip", &z) == -1)