util.paseto: Implementation of PASETO v4.public tokens
PASETO provides an alternative to JWT with the promise of fewer implementation
pitfalls. The v4.public algorithm allows asymmetric cryptographically-verified
token issuance and validation.
In summary, such tokens can be issued by one party and securely verified by
any other party independently using the public key of the issuer. This has a
number of potential applications in a decentralized network and ecosystem such
as XMPP. For example, such tokens could be combined with XEP-0317 to allow
hats to be verified even in the context of a third-party MUC service.
local crypto = require "util.crypto";
local json = require "util.json";
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 s_pack = require "util.struct".pack;
local s_gsub = string.gsub;
local pubkey_methods = {};
local privkey_methods = {};
local v4_public_pubkey_mt = { __index = pubkey_methods };
local v4_public_privkey_mt = { __index = privkey_methods };
local v4_public = {};
local b64url_rep = { ["+"] = "-", ["/"] = "_", ["="] = "", ["-"] = "+", ["_"] = "/" };
local function b64url(data)
return (s_gsub(base64_encode(data), "[+/=]", b64url_rep));
end
local function unb64url(data)
return base64_decode(s_gsub(data, "[-_]", b64url_rep).."==");
end
local function le64(n)
return s_pack("<I8", bit.band(n, 0x7F));
end
local function pae(parts)
local o = { le64(#parts) };
for _, part in ipairs(parts) do
table.insert(o, le64(#part)..part);
end
return table.concat(o);
end
function privkey_methods:export()
return self.key:private_pem();
end
function pubkey_methods:export()
return self.key:public_pem();
end
function v4_public.sign(m, sk, f, i)
if getmetatable(sk) ~= v4_public_privkey_mt then
error("cannot sign v4.public tokens with this key");
end
if type(m) ~= "table" then
return nil, "PASETO payloads must be a table";
end
m = json.encode(m);
local h = "v4.public.";
local m2 = pae({ h, m, f or "", i or "" });
local sig = crypto.ed25519_sign(sk.key, m2);
if not f or f == "" then
return h..b64url(m..sig);
else
return h..b64url(m..sig).."."..b64url(f);
end
end
function v4_public.verify(tok, pk, expected_f, i)
if getmetatable(pk) ~= v4_public_pubkey_mt then
error("cannot verify v4.public tokens with this key");
end
local h, sm, f = tok:match("^(v4%.public%.)([^%.]+)%.?(.*)$");
if not h then
return nil, "invalid-token-format";
end
if expected_f then
if not f or not secure_equals(expected_f, f) then
return nil, "invalid-footer";
end
end
local raw_sm = unb64url(sm);
if not raw_sm or #raw_sm <= 64 then
return nil, "invalid-token-format";
end
local s, m = raw_sm:sub(-64), raw_sm:sub(1, -65);
local m2 = pae({ h, m, f or "", i or "" });
local ok = crypto.ed25519_verify(pk.key, m2, s);
if not ok then
return nil, "invalid-token";
end
local payload, err = json.decode(m);
if err ~= nil or type(payload) ~= "table" then
return nil, "json-decode-error";
end
return payload;
end
function v4_public.new_keypair()
local key = crypto.generate_ed25519_keypair();
return {
private_key = setmetatable({
key = key;
}, v4_public_privkey_mt);
public_key = setmetatable({
key = key;
}, v4_public_pubkey_mt);
};
end
function v4_public.import_public_key(pem)
local key = crypto.import_public_pem(pem);
return setmetatable({
key = key;
}, v4_public_pubkey_mt);
end
function v4_public.import_private_key(pem)
local key = crypto.import_private_pem(pem);
return setmetatable({
key = key;
}, v4_public_privkey_mt);
end
return {
pae = pae;
v4_public = v4_public;
};