From 99748c56551fa06f1f8e3e14c29d8e474724301f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 13 Jan 2023 14:38:05 +0000 Subject: util.paseto: Add support for v3.local tokens --- spec/util_paseto_spec.lua | 209 ++++++++++++++++++++++++++++++++++++++++++---- util/paseto.lua | 98 ++++++++++++++++++++++ 2 files changed, 289 insertions(+), 18 deletions(-) diff --git a/spec/util_paseto_spec.lua b/spec/util_paseto_spec.lua index ec30c6a7..8cad8c72 100644 --- a/spec/util_paseto_spec.lua +++ b/spec/util_paseto_spec.lua @@ -4,28 +4,201 @@ describe("util.paseto", function () local paseto = require "util.paseto"; local json = require "util.json"; + local hex = require "util.hex"; - local function parse_test_cases(json_test_cases) - local input_cases = json.decode(json_test_cases); - local output_cases = {}; - for _, case in ipairs(input_cases) do - assert.is_string(case.name, "Bad test case: expected name"); - assert.is_nil(output_cases[case.name], "Bad test case: duplicate name"); - output_cases[case.name] = function () - local verify_key = paseto.v4_public.import_public_key(case["public-key-pem"]); - local payload, err = paseto.v4_public.verify(case.token, verify_key, case.footer, case["implicit-assertion"]); - if case["expect-fail"] then - assert.is_nil(payload); - else - assert.is_nil(err); - assert.same(json.decode(case.payload), payload); - end - end; + describe("v3.local", function () + local function parse_test_cases(json_test_cases) + local input_cases = json.decode(json_test_cases); + local output_cases = {}; + for _, case in ipairs(input_cases) do + assert.is_string(case.name, "Bad test case: expected name"); + assert.is_nil(output_cases[case.name], "Bad test case: duplicate name"); + output_cases[case.name] = function () + local key = hex.decode(case.key); + local payload, err = paseto.v3_local.decrypt(case.token, key, case.footer, case["implicit-assertion"]); + if case["expect-fail"] then + assert.is_nil(payload); + else + assert.is_nil(err); + assert.same(json.decode(case.payload), payload); + end + end; + end + return output_cases; + end + + local test_cases = parse_test_cases [=[[ + { + "name": "3-E-1", + "expect-fail": false, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAsRm2EsD6yBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9Iza7teRdkiR89ZFyvPPsVjjFiepFUVcMa-LP18zV77f_crJrVXWa5PDNRkCSeHfBBeg", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-2", + "expect-fail": false, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAqhWxBMDgyBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9IzZfaZpReVpHlDSwfuygx1riVXYVs-UjcrG_apl9oz3jCVmmJbRuKn5ZfD8mHz2db0A", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-3", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlxnt5xyhQjFJomwnt7WW_7r2VT0G704ifult011-TgLCyQ2X8imQhniG_hAQ4BydM", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-4", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlBZa_gOpVj4gv0M9lV6Pwjp8JS_MmaZaTA1LLTULXybOBZ2S4xMbYqYmDRhh3IgEk", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-5", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlkYSIbXOgVuIQL65UMdW9WcjOpmqvjqD40NNzed-XPqn1T3w-bJvitYpUJL_rmihc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "3-E-6", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmSeEMphEWHiwtDKJftg41O1F8Hat-8kQ82ZIAMFqkx9q5VkWlxZke9ZzMBbb3Znfo.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "3-E-7", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJkzWACWAIoVa0bz7EWSBoTEnS8MvGBYHHo6t6mJunPrFR9JKXFCc0obwz5N-pxFLOc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "{\"test-vector\":\"3-E-7\"}" + }, + { + "name": "3-E-8", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmZHSSKYR6AnPYJV6gpHtx6dLakIG_AOPhu8vKexNyrv5_1qoom6_NaPGecoiz6fR8.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "{\"test-vector\":\"3-E-8\"}" + }, + { + "name": "3-E-9", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlk1nli0_wijTH_vCuRwckEDc82QWK8-lG2fT9wQF271sgbVRVPjm0LwMQZkvvamqU.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"3-E-9\"}" + }, + { + "name": "3-F-3", + "expect-fail": true, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.1JgN1UG8TFAYS49qsx8rxlwh-9E4ONUm3slJXYi5EibmzxpF0Q-du6gakjuyKCBX8TvnSLOKqCPu8Yh3WSa5yJWigPy33z9XZTJF2HQ9wlLDPtVn_Mu1pPxkTU50ZaBKblJBufRA.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": null, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"3-F-3\"}" + }, + { + "name": "3-F-4", + "expect-fail": true, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAsRm2EsD6yBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9Iza7teRdkiR89ZFyvPPsVjjFiepFUVcMa-LP18zV77f_crJrVXWa5PDNRkCSeHfBBeh", + "payload": null, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-F-5", + "expect-fail": true, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlkYSIbXOgVuIQL65UMdW9WcjOpmqvjqD40NNzed-XPqn1T3w-bJvitYpUJL_rmihc=.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": null, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + } + ]]=]; + for name, test in pairs(test_cases) do + it("test case "..name, test); end - return output_cases; - end + + describe("basic sign/verify", function () + local key = paseto.v3_local.new_key(); + local sign, verify = paseto.v3_local.init(key); + + local key2 = paseto.v3_local.new_key(); + local sign2, verify2 = paseto.v3_local.init(key2); + + it("works", function () + local payload = { foo = "hello world", b = { 1, 2, 3 } }; + + local tok = sign(payload); + assert.same(payload, verify(tok)); + assert.is_nil(verify2(tok)); + end); + + it("rejects tokens if implicit assertion fails", function () + local payload = { foo = "hello world", b = { 1, 2, 3 } }; + local tok = sign(payload, nil, "my-custom-assertion"); + assert.is_nil(verify(tok, nil, "my-incorrect-assertion")); + assert.is_nil(verify(tok, nil, nil)); + assert.same(payload, verify(tok, nil, "my-custom-assertion")); + end); + end); + end); describe("v4.public", function () + local function parse_test_cases(json_test_cases) + local input_cases = json.decode(json_test_cases); + local output_cases = {}; + for _, case in ipairs(input_cases) do + assert.is_string(case.name, "Bad test case: expected name"); + assert.is_nil(output_cases[case.name], "Bad test case: duplicate name"); + output_cases[case.name] = function () + local verify_key = paseto.v4_public.import_public_key(case["public-key-pem"]); + local payload, err = paseto.v4_public.verify(case.token, verify_key, case.footer, case["implicit-assertion"]); + if case["expect-fail"] then + assert.is_nil(payload); + else + assert.is_nil(err); + assert.same(json.decode(case.payload), payload); + end + end; + end + return output_cases; + end + local test_cases = parse_test_cases [=[[ { "name": "4-S-1", diff --git a/util/paseto.lua b/util/paseto.lua index 9ddbe9ad..6cd29f68 100644 --- a/util/paseto.lua +++ b/util/paseto.lua @@ -1,9 +1,12 @@ local crypto = require "util.crypto"; local json = require "util.json"; +local hashes = require "util.hashes"; local base64_encode = require "util.encodings".base64.encode; local base64_decode = require "util.encodings".base64.decode; local secure_equals = require "util.hashes".equals; local bit = require "util.bitcompat"; +local hex = require "util.hex"; +local rand = require "util.random"; local s_pack = require "util.struct".pack; local s_gsub = string.gsub; @@ -114,7 +117,102 @@ function v4_public.new_verifier(public_key_pem, options) return (select(2, v4_public.init(nil, public_key_pem, options))); end +local v3_local = { _key_mt = {} }; + +local function v3_local_derive_keys(k, n) + local tmp = hashes.hkdf_hmac_sha384(48, k, nil, "paseto-encryption-key"..n); + local Ek = tmp:sub(1, 32); + local n2 = tmp:sub(33); + local Ak = hashes.hkdf_hmac_sha384(48, k, nil, "paseto-auth-key-for-aead"..n); + return Ek, Ak, n2; +end + +function v3_local.encrypt(m, k, f, i) + assert(#k == 32) + if type(m) ~= "table" then + return nil, "PASETO payloads must be a table"; + end + m = json.encode(m); + local h = "v3.local."; + local n = rand.bytes(32); + local Ek, Ak, n2 = v3_local_derive_keys(k, n); + + local c = crypto.aes_256_ctr_encrypt(Ek, n2, m); + local m2 = pae({ h, n, c, f or "", i or "" }); + local t = hashes.hmac_sha384(Ak, m2); + + if not f or f == "" then + return h..b64url(n..c..t); + else + return h..b64url(n..c..t).."."..b64url(f); + end +end + +function v3_local.decrypt(tok, k, expected_f, i) + assert(#k == 32) + + local h, sm, f = tok:match("^(v3%.local%.)([^%.]+)%.?(.*)$"); + if not h then + return nil, "invalid-token-format"; + end + f = f and unb64url(f) or nil; + if expected_f then + if not f or not secure_equals(expected_f, f) then + return nil, "invalid-footer"; + end + end + local m = unb64url(sm); + if not m or #m <= 80 then + return nil, "invalid-token-format"; + end + local n, c, t = m:sub(1, 32), m:sub(33, -49), m:sub(-48); + local Ek, Ak, n2 = v3_local_derive_keys(k, n); + local preAuth = pae({ h, n, c, f or "", i or "" }); + local t2 = hashes.hmac_sha384(Ak, preAuth); + if not secure_equals(t, t2) then + return nil, "invalid-token"; + end + local m2 = crypto.aes_256_ctr_decrypt(Ek, n2, c); + if not m2 then + return nil, "invalid-token"; + end + + local payload, err = json.decode(m2); + if err ~= nil or type(payload) ~= "table" then + return nil, "json-decode-error"; + end + return payload; +end + +function v3_local.new_key() + return "secret-token:paseto.v3.local:"..hex.encode(rand.bytes(32)); +end + +function v3_local.init(key, options) + local encoded_key = key:match("^secret%-token:paseto%.v3%.local:(%x+)$"); + if not encoded_key or #encoded_key ~= 64 then + return error("invalid key for v3.local"); + end + local raw_key = hex.decode(encoded_key); + local default_footer = options and options.default_footer; + local default_assertion = options and options.default_implicit_assertion; + return function (token, token_footer, token_assertion) + return v3_local.encrypt(token, raw_key, token_footer or default_footer, token_assertion or default_assertion); + end, function (token, token_footer, token_assertion) + return v3_local.decrypt(token, raw_key, token_footer or default_footer, token_assertion or default_assertion); + end; +end + +function v3_local.new_signer(key, options) + return (v3_local.init(key, options)); +end + +function v3_local.new_verifier(key, options) + return (select(2, v3_local.init(key, options))); +end + return { pae = pae; + v3_local = v3_local; v4_public = v4_public; }; -- cgit v1.2.3