util/paseto.lua
author Matthew Wild <mwild1@gmail.com>
Fri, 24 Jun 2022 17:03:28 +0100
changeset 12698 26a004c96ef8
child 12713 b3f7c77c1f08
permissions -rw-r--r--
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;
};