Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ lua-resty-jwt - [JWT](http://self-issued.info/docs/draft-jones-json-web-token-01
* [set_alg_whitelist](#set_alg_whitelist)
* [set_trusted_certs_file](#set_trusted_certs_file)
* [sign JWE](#sign-jwe)
* [register_zlib_compression](#register_zlib_compression)
* [register_compression_alg](#register_compression_alg)
* [Verification](#verification)
* [JWT Validators](#jwt-validators)
* [Legacy/Timeframe options](#legacy-timeframe-options)
Expand Down Expand Up @@ -199,6 +201,30 @@ sign a table_of_jwt to a jwt_token.
The `alg` argument specifies which key management algorithm to use (`dir`, `RSA-OAEP`, `RSA-OAEP-256`, `ECDH-ES`).
The `enc` argument specifies which content encryption algorithm to use (`A128CBC-HS256`, `A256CBC-HS512`, `A128GCM`, `A256GCM`).

The optional `zip` header parameter (RFC 7516 §4.1.3) enables payload compression
before encryption. **Compression is disabled by default** because compress-then-encrypt
leaks information about the plaintext through the resulting ciphertext length
(CRIME / BREACH family of attacks), and a token that arrives with a `zip` header
will be rejected with `unsupported zip: …` unless a handler has been registered.

To opt in to the built-in `DEF` handler (raw DEFLATE per RFC 1951), install
[lua-zlib](https://github.com/brimworks/lua-zlib) and hand the module to
`jwt:register_zlib_compression` once during startup. Passing the module
explicitly keeps `lua-zlib` an optional dependency of this library and makes
the opt-in step unambiguous:

```
luarocks install lua-zlib
```

```lua
local jwt = require "resty.jwt"
jwt:register_zlib_compression(require "zlib")
```

Alternatively, register your own handler (pure-Lua, FFI, or a different alg
name entirely) via `jwt:register_compression_alg` — see below.

### sample of table_of_jwt ###

```
Expand All @@ -208,6 +234,55 @@ The `enc` argument specifies which content encryption algorithm to use (`A128CBC
}
```

### sample with DEFLATE compression ###

```
{
"header": {"typ": "JWE", "alg": "dir", "enc":"A128CBC-HS256", "zip": "DEF"},
"payload": {"foo": "bar"}
}
```

## register_zlib_compression

`syntax: jwt:register_zlib_compression(zlib_module)`

Register the `DEF` (raw DEFLATE per RFC 1951) compression handler using a
caller-supplied [lua-zlib](https://github.com/brimworks/lua-zlib)-compatible
module. Passing the module explicitly keeps `lua-zlib` an optional dependency
and makes JWE compression opt-in; see the security note under
[sign-jwe](#sign-jwe).

```lua
jwt:register_zlib_compression(require "zlib")
```

## register_compression_alg

`syntax: jwt:register_compression_alg(name, { deflate = fn, inflate = fn })`

Register or override the handler used for a given JWE `zip` header value. Use
this to swap in an alternate DEFLATE implementation (pure-Lua, FFI, etc.) or to
support a non-standard `zip` value. No `zip` handler is registered out of the
box — see [register_zlib_compression](#register_zlib_compression) for the
standard `DEF` case.

`deflate` and `inflate` each take a byte string and must return either a byte
string on success, or `nil, err` on failure.

```lua
jwt:register_compression_alg("DEF", {
deflate = function(data) return my_compress(data) end,
inflate = function(data) return my_decompress(data) end,
})
```

For a concrete reference implementation, see how
[`register_zlib_compression`](#register_zlib_compression) wires `lua-zlib` into
this API in `lib/resty/jwt.lua` — it builds the `{ deflate, inflate }` pair
around the zlib streaming API (with `windowBits = -15` for raw DEFLATE per
RFC 1951) and hands it straight to `register_compression_alg`.

[Back to TOC](#table-of-contents)


Expand Down
2 changes: 1 addition & 1 deletion ci
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ docker run \
-w /lua-resty-jwt \
--name lua-resty-jwt-tests \
"$IMAGE" \
-c 'luarocks make lua-resty-jwt-dev-0.rockspec && prove -j4 -r t; rc=$?; rm -rf t/servroot_* 2>/dev/null; exit $rc'
-c '(dpkg -s zlib1g-dev >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y --no-install-recommends zlib1g-dev)) && luarocks install lua-zlib 1.3-0 && luarocks make lua-resty-jwt-dev-0.rockspec && prove -j4 -r t; rc=$?; rm -rf t/servroot_* 2>/dev/null; exit $rc'
2 changes: 2 additions & 0 deletions ci-coverage
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ docker run \
--name lua-resty-jwt-coverage \
"$IMAGE" \
-c '
dpkg -s zlib1g-dev >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y --no-install-recommends zlib1g-dev)
luarocks install luacov
luarocks install lua-zlib 1.3-0
luarocks make lua-resty-jwt-dev-0.rockspec

rm -f luacov.stats.out luacov.report.out
Expand Down
90 changes: 90 additions & 0 deletions lib/resty/jwt.lua
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ local str_const = {
PBES2_HS384_A192KW = "PBES2-HS384+A192KW",
PBES2_HS512_A256KW = "PBES2-HS512+A256KW",
DIR = "dir",
zip = "zip",
DEF = "DEF",
reason = "reason",
verified = "verified",
number = "number",
Expand Down Expand Up @@ -485,6 +487,15 @@ local function get_payload_decoder(self)
return self.payload_decoder or cjson_decode
end

-- Registry for JWE "zip" header parameter handlers (RFC 7516 §4.1.3).
-- Each handler is a table { deflate = fn(bytes)->bytes,err inflate = fn(bytes)->bytes,err }.
-- Intentionally empty by default: compression-then-encryption is vulnerable to
-- CRIME/BREACH-style side-channel attacks when an attacker can influence part of
-- the plaintext, so callers must explicitly opt in — either via
-- jwt:register_zlib_compression(require "zlib") (which binds "DEF" to lua-zlib)
-- or by registering their own handler with jwt:register_compression_alg.
local compression_algs = {}

--@function parse_jwe
--@param pre-shared key
--@encoded-header
Expand All @@ -507,6 +518,11 @@ local function parse_jwe(self, preshared_key, encoded_header, encoded_encrypted_
error({reason="invalid algorithm: " .. alg})
end

-- Fail fast on unsupported compression before doing any expensive crypto work.
if header.zip and not compression_algs[header.zip] then
error({reason="unsupported zip: " .. header.zip})
end

local key, enc_key, _
if alg == str_const.DIR then
if not preshared_key then
Expand Down Expand Up @@ -640,6 +656,17 @@ local function parse_jwe(self, preshared_key, encoded_header, encoded_encrypted_
error({reason="failed to decrypt payload: " .. err})

else
if header.zip then
local handler = compression_algs[header.zip]
if not handler then
error({reason="unsupported zip: " .. header.zip})
end
local inflated, zerr = handler.inflate(payload)
if zerr or not inflated then
error({reason="failed to decompress payload: " .. (zerr or "unknown error")})
end
payload = inflated
end
basic_jwe.payload = get_payload_decoder(self)(payload)
basic_jwe.internal.json_payload=payload
end
Expand Down Expand Up @@ -812,6 +839,17 @@ local function sign_jwe(self, secret_key, jwt_obj)
local key, encrypted_key, mac_key, enc_key, _
local encoded_header = _M:jwt_encode(header)
local payload_to_encrypt = get_payload_encoder(self)(jwt_obj.payload)
if header.zip then
local handler = compression_algs[header.zip]
if not handler then
error({reason="unsupported zip: " .. header.zip})
end
local compressed, zerr = handler.deflate(payload_to_encrypt)
if zerr or not compressed then
error({reason="failed to compress payload: " .. (zerr or "unknown error")})
end
payload_to_encrypt = compressed
end
if alg == str_const.DIR then
_, mac_key, enc_key = derive_keys(enc, secret_key)
encrypted_key = ""
Expand Down Expand Up @@ -1440,6 +1478,58 @@ function _M.set_payload_decoder(self, decoder)
end


--@function register_compression_alg : register a handler for the given JWE "zip" header value
--@param name : the `zip` header value to bind (e.g. "DEF")
--@param handler : a table { deflate = fn(bytes)->bytes,err inflate = fn(bytes)->bytes,err }
function _M.register_compression_alg(self, name, handler)
if type(name) ~= "string" or name == "" then
error({reason="compression alg name must be a non-empty string"})
end
if type(handler) ~= "table"
or type(handler.deflate) ~= "function"
or type(handler.inflate) ~= "function" then
error({reason="compression handler must be a table with deflate and inflate functions"})
end
compression_algs[name] = handler
end


--@function register_zlib_compression : bind the JWE "DEF" zip alg to a caller-supplied lua-zlib module
--@param zlib : a lua-zlib-compatible module (typically the result of `require "zlib"`).
-- Passing it in keeps the dependency caller-owned and makes the call itself the opt-in.
-- JWE compression is disabled by default because compress-then-encrypt leaks
-- information about plaintext through ciphertext length (CRIME / BREACH family);
-- only enable it when attacker-chosen plaintext cannot be mixed with secrets.
-- Note: DEFLATE can expand modest inputs into very large outputs ("decompression
-- bombs"); consumers that accept untrusted JWEs should bound the ciphertext size
-- before calling verify/load to keep the inflate step's memory cost predictable.
function _M.register_zlib_compression(self, zlib)
if type(zlib) ~= "table"
or type(zlib.deflate) ~= "function"
or type(zlib.inflate) ~= "function" then
error({reason="zlib module must expose deflate and inflate functions (pass `require \"zlib\"`)"})
end
_M.register_compression_alg(self, str_const.DEF, {
deflate = function(data)
local stream = zlib.deflate(zlib.BEST_COMPRESSION, -15)
local ok, compressed = pcall(stream, data, "finish")
if not ok then
return nil, tostring(compressed)
end
return compressed
end,
inflate = function(data)
local stream = zlib.inflate(-15)
local ok, decompressed = pcall(stream, data, "finish")
if not ok then
return nil, tostring(decompressed)
end
return decompressed
end,
})
end


function _M.new()
return setmetatable({}, mt)
end
Expand Down
Loading