Files
upstream-jitsi-meet/tests/prosody/lua/luajwtjitsi_spec.lua
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

444 lines
18 KiB
Lua

-- Unit tests for luajwtjitsi.lib.lua
-- Run with busted from resources/prosody-plugins/:
-- busted spec/lua/
-- Requires: basexx, lua-cjson, openssl (luarocks package 'openssl')
-- Compatibility shim: the luarocks 'openssl' package (v0.11+) has a different
-- API from the old 'lua-openssl' that the production code was written for.
-- Differences handled here:
-- 1. require style: monolithic (require 'openssl') vs submodule (require 'openssl.digest')
-- 2. hmac.new: old=(key,algo), new=(algo,key)
-- 3. digest:update(): old returns self (chainable), new returns bool
-- 4. pkey.new(pem): not supported in new API; use pkey.read(pem, private, 'pem')
-- 5. pkey:sign(digest_obj): old takes digest obj, new takes (data, algo)
-- 6. pkey:verify(sig, digest_obj): old order; new is verify(data, sig, algo)
local _ok, _ssl = pcall(require, 'openssl')
if _ok and type(_ssl) == 'table' then
-- 1+2. hmac: swap args and register as submodule
if not package.preload['openssl.hmac'] then
local _hmac_raw = _ssl.hmac
local _hmac_compat = setmetatable({}, { __index = _hmac_raw })
_hmac_compat.new = function(key, algo) return _hmac_raw.new(algo, key) end
package.preload['openssl.hmac'] = function() return _hmac_compat end
end
-- 1+3. digest: make :update() return self, store data+algo for pkey compat
if not package.preload['openssl.digest'] then
local _digest_raw = _ssl.digest
local _digest_compat = {}
_digest_compat.new = function(algo)
local inner = _digest_raw.new(algo)
local w = { _inner = inner, _algo = algo, _data = '' }
return setmetatable(w, {
__index = function(self, k)
if k == 'update' then
return function(s, d)
s._inner:update(d)
s._data = s._data .. d
return s -- return self for chaining (old API)
end
elseif k == 'final' then
return function(s) return s._inner:final() end
end
end
})
end
package.preload['openssl.digest'] = function() return _digest_compat end
end
-- 1+4+5+6. pkey: map new() to read(), adapt sign/verify signatures
if not package.preload['openssl.pkey'] then
local _pkey_raw = _ssl.pkey
local function wrap_key(raw)
return setmetatable({}, {
__index = function(_, k)
if k == 'sign' then
-- old: sign(digest_obj) → new: sign(data, algo)
return function(_, dobj)
return raw:sign(dobj._data, dobj._algo)
end
elseif k == 'verify' then
-- old: verify(sig, digest_obj) → new: verify(data, sig, algo)
return function(_, sig, dobj)
return raw:verify(dobj._data, sig, dobj._algo)
end
else
return raw[k]
end
end
})
end
local _pkey_compat = setmetatable({}, { __index = _pkey_raw })
_pkey_compat.new = function(pem)
local ok1, k1 = pcall(_pkey_raw.read, pem, true, 'pem') -- private
if ok1 and k1 then return wrap_key(k1) end
local ok2, k2 = pcall(_pkey_raw.read, pem, false, 'pem') -- public
if ok2 and k2 then return wrap_key(k2) end
return nil -- signal invalid key (matches old API behaviour)
end
package.preload['openssl.pkey'] = function() return _pkey_compat end
end
end
-- Guard: skip the whole suite if the module's hard dependencies aren't installed.
local ok, M = pcall(dofile, "luajwtjitsi.lib.lua")
if not ok then
describe("luajwtjitsi", function()
it("skipped — missing dependencies (lua-cjson and/or lua-openssl not installed)", function()
pending(tostring(M):match("([^\n]+)"))
end)
end)
return
end
local I = M._internals
-- Test RSA key pair (1024-bit PKCS#1, test use only — not for production).
-- Generated with: openssl genrsa -traditional 1024
local PRIVATE_PEM = [[
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDElXxE6H6t622jPZbwgXi+WuJpLDIPr2znPUqtzTJCAVnK1+Iw
CoTjKYmtkiaszVCn/QL41Hr1JXHA/zzo/dLedvKQhs6dvadUVt4Ch2D7hcqGzn7T
oCTCzD1CyaIb9jGR0KOh8gMpL7JE6Zk9f1ku+91QPTe55FlyHPJ4fZfh/wIDAQAB
AoGBAK6uUlV87owmxi6m3MfrlFo5sjRYhEdxz2iPCxvzWTRTtPFM8f1Ua47bay6j
zuAANSqA5bIsRPIvCqy/YOV8HnHnNVl1ARCEAWMl2it0BsqOxuJKhqUsbp1gPAtM
mJE51yPwfN8fcvMNw+6dwV0h3jp+Y57RR0JAhMRWZNrBSmPhAkEA4smEZX8tjQzB
W3FoPIz5a8hgTDfyHYZftC6HqTdEx/NK1z2+EkirS/4WAcMkIb63gWtxSs/9H+bE
tEKcIGzJsQJBAN3n/iLa+2SIseykvzD5bJrdqdmpFYTMzzs8mua5ngkVGd5hkhJ1
MhhfRe3B/ytBLsd74A889k6vFUxoIYMYoq8CQDinhv+kCckWyqcBabzWquiWIhho
UX+2tenZReKr/+7DhIrIzdbbeI9/ZhgCm+AiOM2H/cFmvCFgEPvHOCGRi8ECQQC3
qIe0j2sbZOtlgWDgBr+1WRp3lDLEK8KkM1+88Al1WOqFHFoKPscMVuYIhtRLF3LB
Fax20CzHKK9HArQhzWTvAkBezAcZyo/TBNq+gHKAQw66mFbr7QSd7BCMIQAZg6Cn
VfvTq/eTsOOIgqKYyQzlSY1Jwu27pVQtRzGPOitHbO3E
-----END RSA PRIVATE KEY-----
]]
local PUBLIC_PEM = [[
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDElXxE6H6t622jPZbwgXi+WuJp
LDIPr2znPUqtzTJCAVnK1+IwCoTjKYmtkiaszVCn/QL41Hr1JXHA/zzo/dLedvKQ
hs6dvadUVt4Ch2D7hcqGzn7ToCTCzD1CyaIb9jGR0KOh8gMpL7JE6Zk9f1ku+91Q
PTe55FlyHPJ4fZfh/wIDAQAB
-----END PUBLIC KEY-----
]]
-- Returns a payload with exp set one hour in the future (so verify's exp check passes).
-- Extra fields can be merged in via the `extra` table.
local function payload(extra)
local p = { sub = 'test', exp = os.time() + 3600 }
if extra then
for k, v in pairs(extra) do p[k] = v end
end
return p
end
describe("luajwtjitsi", function()
describe("internals", function()
describe("verify_claim", function()
it("exact match returns true", function()
assert.is_true(I.verify_claim('foo', { 'foo', 'bar' }))
end)
it("non-matching value returns false", function()
assert.is_false(I.verify_claim('baz', { 'foo', 'bar' }))
end)
it("wildcard '*' matches any value", function()
assert.is_true(I.verify_claim('anything', { '*' }))
end)
it("empty list returns false", function()
assert.is_false(I.verify_claim('foo', {}))
end)
it("wildcard in mixed list still matches", function()
assert.is_true(I.verify_claim('x', { 'a', '*', 'b' }))
end)
end)
describe("split_token", function()
it("splits three segments correctly", function()
local parts = I.split_token("aaa.bbb.ccc")
assert.equal(3, #parts)
assert.equal('aaa', parts[1])
assert.equal('bbb', parts[2])
assert.equal('ccc', parts[3])
end)
it("returns one element when there are no dots", function()
local parts = I.split_token("nodots")
assert.equal(1, #parts)
assert.equal('nodots', parts[1])
end)
it("returns two elements for two segments", function()
local parts = I.split_token("hdr.body")
assert.equal(2, #parts)
end)
end)
describe("strip_signature", function()
it("removes the signature segment", function()
local result = I.strip_signature("hdr.body.sig")
assert.equal("hdr.body", result)
end)
it("returns nil and error for two-segment input", function()
local result, _, _, err = I.strip_signature("hdr.body")
assert.is_nil(result)
assert.is_string(err)
end)
it("returns nil and error for single-segment input", function()
local result, _, _, err = I.strip_signature("nodots")
assert.is_nil(result)
assert.is_string(err)
end)
end)
describe("parse_token", function()
it("parses a valid HS256 token into header, body, and signature", function()
local tok = assert(M.encode({ sub = 'alice', exp = os.time() + 3600 }, 'secret', 'HS256'))
local header, body, sig = I.parse_token(tok)
assert.is_table(header)
assert.is_table(body)
assert.is_string(sig)
assert.equal('JWT', header.typ)
assert.equal('HS256', header.alg)
assert.equal('alice', body.sub)
end)
it("returns error for token with only two segments", function()
local h, b, s, err = I.parse_token("hdr.body")
assert.is_nil(h)
assert.is_string(err)
end)
it("returns error or throws for token with invalid base64 header", function()
-- Older basexx returns garbage bytes (decode attempt fails gracefully);
-- newer basexx returns nil causing cjson.decode to throw. Both are
-- acceptable outcomes — just verify we never get a valid header back.
local ok, h = pcall(I.parse_token, "!!!!.body.sig")
if ok then
assert.is_nil(h) -- graceful nil-return path
end
-- if not ok: threw an error, which is also acceptable
end)
it("returns error for token with invalid JSON body", function()
local basexx = require 'basexx'
local good_hdr = basexx.to_url64('{"typ":"JWT","alg":"HS256"}')
local bad_body = basexx.to_url64('not-valid-json{{')
local h, b, s, err = I.parse_token(good_hdr .. '.' .. bad_body .. '.sig')
assert.is_nil(h)
assert.is_string(err)
end)
end)
end) -- internals
describe("encode", function()
describe("argument validation", function()
it("non-table data returns error", function()
local tok, err = M.encode("not-a-table", "key", "HS256")
assert.is_nil(tok)
assert.is_string(err)
end)
it("non-string key returns error", function()
local tok, err = M.encode({}, 123, "HS256")
assert.is_nil(tok)
assert.is_string(err)
end)
it("unsupported algorithm returns error", function()
local tok, err = M.encode({}, "key", "XX999")
assert.is_nil(tok)
assert.is_string(err)
end)
end)
end) -- encode
describe("verify", function()
describe("argument validation", function()
it("non-string token returns error", function()
local body, err = M.verify(123, "HS256", "key")
assert.is_nil(body)
assert.is_string(err)
end)
it("non-string algorithm returns error", function()
local body, err = M.verify("a.b.c", 42, "key")
assert.is_nil(body)
assert.is_string(err)
end)
it("unsupported algorithm returns error", function()
local body, err = M.verify("a.b.c", "XX999", "key")
assert.is_nil(body)
assert.is_string(err)
end)
it("malformed token (two segments) returns error", function()
local body, err = M.verify("hdr.body", "HS256", "key")
assert.is_nil(body)
assert.is_string(err)
end)
end)
describe("HS256", function()
it("encode then verify returns original payload", function()
local tok = assert(M.encode(payload({ iss = 'me' }), 'my-secret', 'HS256'))
local body = assert(M.verify(tok, 'HS256', 'my-secret'))
assert.equal('test', body.sub)
assert.equal('me', body.iss)
end)
it("wrong key fails verification", function()
local tok = assert(M.encode(payload(), 'correct', 'HS256'))
local body, err = M.verify(tok, 'HS256', 'wrong')
assert.is_nil(body)
assert.is_string(err)
end)
it("tampered payload fails verification", function()
local tok = assert(M.encode(payload(), 'secret', 'HS256'))
local basexx = require 'basexx'
local parts = I.split_token(tok)
parts[2] = basexx.to_url64('{"sub":"hacker","exp":9999999999}')
local body, err = M.verify(table.concat(parts, '.'), 'HS256', 'secret')
assert.is_nil(body)
assert.is_string(err)
end)
end)
describe("HS384", function()
it("encode then verify returns original payload", function()
local tok = assert(M.encode(payload(), 'secret', 'HS384'))
local body = assert(M.verify(tok, 'HS384', 'secret'))
assert.equal('test', body.sub)
end)
end)
describe("HS512", function()
it("encode then verify returns original payload", function()
local tok = assert(M.encode(payload(), 'secret', 'HS512'))
local body = assert(M.verify(tok, 'HS512', 'secret'))
assert.equal('test', body.sub)
end)
end)
describe("RS256", function()
it("encode with private key, verify with public key", function()
local tok = assert(M.encode(payload(), PRIVATE_PEM, 'RS256'))
local body = assert(M.verify(tok, 'RS256', PUBLIC_PEM))
assert.equal('test', body.sub)
end)
it("tampered signature fails verification", function()
local tok = assert(M.encode(payload(), PRIVATE_PEM, 'RS256'))
-- Corrupt the middle of the signature segment so the decoded bytes change.
-- (Avoid the last 1-2 chars: they may carry padding bits that decode identically.)
local mid = math.floor(#tok / 2)
local c = tok:sub(mid, mid)
local replacement = (c == 'A') and 'B' or 'A'
local tampered = tok:sub(1, mid - 1) .. replacement .. tok:sub(mid + 1)
local body, err = M.verify(tampered, 'RS256', PUBLIC_PEM)
assert.is_nil(body)
assert.is_string(err)
end)
end)
describe("claims", function()
it("expired token is rejected", function()
local tok = assert(M.encode({ sub = 'test', exp = os.time() - 1 }, 'secret', 'HS256'))
local body, err = M.verify(tok, 'HS256', 'secret')
assert.is_nil(body)
assert.truthy(err:match('exp'))
end)
it("nbf in the future is rejected", function()
local tok = assert(M.encode(
{ sub = 'test', exp = os.time() + 3600, nbf = os.time() + 9999 },
'secret', 'HS256'
))
local body, err = M.verify(tok, 'HS256', 'secret')
assert.is_nil(body)
assert.truthy(err:match('nbf'))
end)
it("alg mismatch between header and expectedAlgo is rejected", function()
local tok = assert(M.encode(payload(), 'secret', 'HS256'))
local body, err = M.verify(tok, 'HS512', 'secret')
assert.is_nil(body)
assert.is_string(err)
end)
end)
describe("issuer", function()
it("accepted issuer passes", function()
local tok = assert(M.encode(payload({ iss = 'jitsi' }), 'secret', 'HS256'))
local body = assert(M.verify(tok, 'HS256', 'secret', { 'jitsi' }))
assert.equal('jitsi', body.iss)
end)
it("wrong issuer is rejected", function()
local tok = assert(M.encode(payload({ iss = 'evil' }), 'secret', 'HS256'))
local body, err = M.verify(tok, 'HS256', 'secret', { 'jitsi' })
assert.is_nil(body)
assert.is_string(err)
end)
it("missing iss claim when acceptedIssuers provided is rejected", function()
local tok = assert(M.encode(payload(), 'secret', 'HS256'))
local body, err = M.verify(tok, 'HS256', 'secret', { 'jitsi' })
assert.is_nil(body)
assert.is_string(err)
end)
it("wildcard matches any value", function()
local tok = assert(M.encode(payload({ iss = 'anyone' }), 'secret', 'HS256'))
local body = assert(M.verify(tok, 'HS256', 'secret', { '*' }))
assert.equal('anyone', body.iss)
end)
end)
describe("audience", function()
it("accepted audience passes", function()
local tok = assert(M.encode(payload({ aud = 'jitsi' }), 'secret', 'HS256'))
local body = assert(M.verify(tok, 'HS256', 'secret', nil, { 'jitsi' }))
assert.equal('jitsi', body.aud)
end)
it("wrong audience is rejected", function()
local tok = assert(M.encode(payload({ aud = 'wrong' }), 'secret', 'HS256'))
local body, err = M.verify(tok, 'HS256', 'secret', nil, { 'jitsi' })
assert.is_nil(body)
assert.is_string(err)
end)
it("missing aud claim when acceptedAudiences provided is rejected", function()
local tok = assert(M.encode(payload(), 'secret', 'HS256'))
local body, err = M.verify(tok, 'HS256', 'secret', nil, { 'jitsi' })
assert.is_nil(body)
assert.is_string(err)
end)
it("wildcard matches any value", function()
local tok = assert(M.encode(payload({ aud = 'anything' }), 'secret', 'HS256'))
local body = assert(M.verify(tok, 'HS256', 'secret', nil, { '*' }))
assert.equal('anything', body.aud)
end)
end)
end) -- verify
end) -- luajwtjitsi