Skip to content

Commit 4d25e04

Browse files
committed
fix: multi-round Concat KDF for ECDH-ES with >256-bit enc keys
The Concat KDF (RFC 7518 §4.6.2) only performed a single SHA-256 round, breaking ECDH-ES direct key agreement with A192CBC-HS384 (384-bit) and A256CBC-HS512 (512-bit) content encryption algorithms. Added 21 JWE tests covering dir+GCM, ECDH-ES+CBC, P-384 curve, wrong-key rejection, claim validation, and edge cases. Added luacov coverage tooling (ci-coverage script, 87% line coverage). 858 tests passing.
1 parent 0d1ebea commit 4d25e04

14 files changed

Lines changed: 839 additions & 15 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
*swp
22
t/servroot
33
.DS_Store
4+
luacov.stats.out
5+
luacov.report.out

.luacov

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- luacov configuration
2+
-- Only instrument our library files, not third-party or test code
3+
return {
4+
statsfile = "luacov.stats.out",
5+
reportfile = "luacov.report.out",
6+
7+
include = {
8+
"resty/jwt$",
9+
"resty/evp$",
10+
"resty/jwt%-validators$",
11+
"resty/hmac$",
12+
"resty/utils$",
13+
},
14+
15+
exclude = {
16+
"luacov$",
17+
"luacov/",
18+
"cjson",
19+
"resty/openssl",
20+
"resty/random",
21+
},
22+
}

ci-coverage

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/bin/bash
2+
set -eo pipefail
3+
4+
IMAGE="cdbattags/openresty-testsuite:latest"
5+
6+
docker run \
7+
-i --rm \
8+
--entrypoint=/bin/sh \
9+
-v "$(pwd)":/lua-resty-jwt \
10+
-w /lua-resty-jwt \
11+
--name lua-resty-jwt-coverage \
12+
"$IMAGE" \
13+
-c '
14+
luarocks install luacov
15+
luarocks make lua-resty-jwt-dev-0.rockspec
16+
17+
rm -f luacov.stats.out luacov.report.out
18+
19+
COVERAGE=1 prove -j1 -r t
20+
rc=$?
21+
22+
luacov
23+
echo ""
24+
echo "========================================"
25+
echo " Coverage Report"
26+
echo "========================================"
27+
cat luacov.report.out
28+
29+
rm -rf t/servroot_* 2>/dev/null
30+
exit $rc
31+
'

lib/resty/jwt.lua

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ local openssl_rand = require "resty.openssl.rand"
1010
local kdf = require "resty.openssl.kdf"
1111
local utils = require "resty.utils"
1212

13-
local _M = { _VERSION = "0.3.0" }
13+
local _M = { _VERSION = "0.3.1" }
1414

1515
local mt = {
1616
__index = _M
@@ -187,7 +187,8 @@ local ec_nid_to_curve = {
187187
[716] = "secp521r1",
188188
}
189189

190-
-- RFC 7518 Section 4.6.2 - Concat KDF (single-pass SHA-256)
190+
-- RFC 7518 Section 4.6.2 - Concat KDF (multi-round SHA-256)
191+
-- reps = ceil(keydatalen / hashlen); hashlen = 256 for SHA-256
191192
local function derive_shared_key(header, shared_secret_Z)
192193
local enc = header.enc
193194
local keydatalen = keydatalen_map[enc]
@@ -219,21 +220,32 @@ local function derive_shared_key(header, shared_secret_Z)
219220

220221
utils.append_array(other_info, utils.integer_to_32_bit_big_endian(keydatalen))
221222

223+
local hashlen = 256
224+
local reps = math.ceil(keydatalen / hashlen)
222225
local z_bytes = utils.string_to_byte_array(shared_secret_Z)
223-
local round_concat = utils.append_array({0, 0, 0, 1}, z_bytes)
224-
utils.append_array(round_concat, other_info)
225-
226-
local input = string_char(unpack(round_concat))
227-
local d, err = digest.new("SHA256")
228-
if not d then
229-
error({reason="failed to create SHA256 digest: " .. (err or "")})
230-
end
231-
local md, hash_err = d:final(input)
232-
if not md then
233-
error({reason="failed to compute KDF hash: " .. (hash_err or "")})
226+
local derived = {}
227+
228+
for round = 1, reps do
229+
local counter = utils.integer_to_32_bit_big_endian(round)
230+
local round_concat = {}
231+
utils.append_array(round_concat, counter)
232+
utils.append_array(round_concat, z_bytes)
233+
utils.append_array(round_concat, other_info)
234+
235+
local input = string_char(unpack(round_concat))
236+
local d, err = digest.new("SHA256")
237+
if not d then
238+
error({reason="failed to create SHA256 digest: " .. (err or "")})
239+
end
240+
local md, hash_err = d:final(input)
241+
if not md then
242+
error({reason="failed to compute KDF hash: " .. (hash_err or "")})
243+
end
244+
derived[round] = md
234245
end
235246

236-
return string_sub(md, 1, keydatalen / 8)
247+
local full = table_concat(derived)
248+
return string_sub(full, 1, keydatalen / 8)
237249
end
238250

239251
-- AES Key Wrap (RFC 3394) default IV

t/function-secret.t

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ our $HttpConfig = <<'_EOC_';
99
lua_package_path 'lib/?.lua;;';
1010
_EOC_
1111
12+
if ($ENV{COVERAGE}) {
13+
$HttpConfig .= " init_by_lua_block { require('luacov') }\n";
14+
}
15+
1216
no_long_string();
1317
1418
run_tests();

0 commit comments

Comments
 (0)