Changeset

12694:26a004c96ef8

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.
author Matthew Wild <mwild1@gmail.com>
date Fri, 24 Jun 2022 17:03:28 +0100
parents 12693:7c5afbdcbc77
children 12695:6aaa604fdfd5
files util/paseto.lua
diffstat 1 files changed, 123 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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;
+};