util.paseto: Implementation of PASETO v4.public tokens
authorMatthew Wild <mwild1@gmail.com>
Fri, 24 Jun 2022 17:03:28 +0100
changeset 12698 26a004c96ef8
parent 12697 7c5afbdcbc77
child 12699 6aaa604fdfd5
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.
util/paseto.lua
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/paseto.lua	Fri Jun 24 17:03:28 2022 +0100
@@ -0,0 +1,123 @@
+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;
+};