aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--spec/util_jwt_spec.lua50
-rw-r--r--util/jwt.lua140
2 files changed, 171 insertions, 19 deletions
diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua
index b391a870..854688bd 100644
--- a/spec/util_jwt_spec.lua
+++ b/spec/util_jwt_spec.lua
@@ -16,5 +16,55 @@ describe("util.jwt", function ()
local ok = jwt.verify(key, token);
assert.falsy(ok)
end);
+
+ it("validates ES256", function ()
+ local private_key = [[
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
+OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
+1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
+-----END PRIVATE KEY-----
+]];
+
+ local sign = jwt.new_signer("ES256", private_key);
+
+ local token = sign({
+ sub = "1234567890";
+ name = "John Doe";
+ admin = true;
+ iat = 1516239022;
+ });
+
+ local public_key = [[
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
+q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
+-----END PUBLIC KEY-----
+]];
+ local verify = jwt.new_verifier("ES256", public_key);
+
+ local result = {verify(token)};
+ assert.same({
+ true; -- success
+ { -- payload
+ sub = "1234567890";
+ name = "John Doe";
+ admin = true;
+ iat = 1516239022;
+ };
+ }, result);
+
+ local result = {verify[[eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA]]};
+ assert.same({
+ true; -- success
+ { -- payload
+ sub = "1234567890";
+ name = "John Doe";
+ admin = true;
+ iat = 1516239022;
+ };
+ }, result);
+ end);
+
end);
diff --git a/util/jwt.lua b/util/jwt.lua
index bf106dfa..58888b5d 100644
--- a/util/jwt.lua
+++ b/util/jwt.lua
@@ -1,4 +1,5 @@
local s_gsub = string.gsub;
+local crypto = require "util.crypto";
local json = require "util.json";
local hashes = require "util.hashes";
local base64_encode = require "util.encodings".base64.encode;
@@ -13,17 +14,8 @@ local function unb64url(data)
return base64_decode(s_gsub(data, "[-_]", b64url_rep).."==");
end
-local static_header = b64url('{"alg":"HS256","typ":"JWT"}') .. '.';
-
-local function sign(key, payload)
- local encoded_payload = json.encode(payload);
- local signed = static_header .. b64url(encoded_payload);
- local signature = hashes.hmac_sha256(key, signed);
- return signed .. "." .. b64url(signature);
-end
-
local jwt_pattern = "^(([A-Za-z0-9-_]+)%.([A-Za-z0-9-_]+))%.([A-Za-z0-9-_]+)$"
-local function verify(key, blob)
+local function decode_jwt(blob, expected_alg)
local signed, bheader, bpayload, signature = string.match(blob, jwt_pattern);
if not signed then
return nil, "invalid-encoding";
@@ -31,21 +23,131 @@ local function verify(key, blob)
local header = json.decode(unb64url(bheader));
if not header or type(header) ~= "table" then
return nil, "invalid-header";
- elseif header.alg ~= "HS256" then
+ elseif header.alg ~= expected_alg then
return nil, "unsupported-algorithm";
end
- if not secure_equals(b64url(hashes.hmac_sha256(key, signed)), signature) then
- return false, "signature-mismatch";
+ return signed, signature, bpayload;
+end
+
+local function new_static_header(algorithm_name)
+ return b64url('{"alg":"'..algorithm_name..'","typ":"JWT"}') .. '.';
+end
+
+-- HS*** family
+local function new_hmac_algorithm(name, hmac)
+ local static_header = new_static_header(name);
+
+ local function sign(key, payload)
+ local encoded_payload = json.encode(payload);
+ local signed = static_header .. b64url(encoded_payload);
+ local signature = hmac(key, signed);
+ return signed .. "." .. b64url(signature);
+ end
+
+ local function verify(key, blob)
+ local signed, signature, raw_payload = decode_jwt(blob, name);
+ if not signed then return nil, signature; end -- nil, err
+
+ if not secure_equals(b64url(hmac(key, signed)), signature) then
+ return false, "signature-mismatch";
+ end
+ local payload, err = json.decode(unb64url(raw_payload));
+ if err ~= nil then
+ return nil, "json-decode-error";
+ end
+ return true, payload;
end
- local payload, err = json.decode(unb64url(bpayload));
- if err ~= nil then
- return nil, "json-decode-error";
+
+ local function load_key(key)
+ assert(type(key) == "string", "key must be string (long, random, secure)");
+ return key;
+ end
+
+ return { sign = sign, verify = verify, load_key = load_key };
+end
+
+-- ES*** family
+local function new_ecdsa_algorithm(name, c_sign, c_verify)
+ local static_header = new_static_header(name);
+
+ return {
+ sign = function (private_key, payload)
+ local encoded_payload = json.encode(payload);
+ local signed = static_header .. b64url(encoded_payload);
+
+ local der_sig = c_sign(private_key, signed);
+
+ local r, s = crypto.parse_ecdsa_signature(der_sig);
+
+ return signed.."."..b64url(r..s);
+ end;
+
+ verify = function (public_key, blob)
+ local signed, signature, raw_payload = decode_jwt(blob, name);
+ if not signed then return nil, signature; end -- nil, err
+
+ local raw_signature = unb64url(signature);
+
+ local der_sig = crypto.build_ecdsa_signature(raw_signature:sub(1, 32), raw_signature:sub(33, 64));
+ if not der_sig then
+ return false, "signature-mismatch";
+ end
+
+ local verify_ok = c_verify(public_key, signed, der_sig);
+ if not verify_ok then
+ return false, "signature-mismatch";
+ end
+
+ local payload, err = json.decode(unb64url(raw_payload));
+ if err ~= nil then
+ return nil, "json-decode-error";
+ end
+
+ return true, payload;
+ end;
+
+ load_public_key = function (public_key_pem)
+ local key = assert(crypto.import_public_pem(public_key_pem));
+ assert(key:get_type() == "id-ecPublicKey", "incorrect key type");
+ return key;
+ end;
+
+ load_private_key = function (private_key_pem)
+ local key = assert(crypto.import_private_pem(private_key_pem));
+ assert(key:get_type() == "id-ecPublicKey", "incorrect key type");
+ return key;
+ end;
+ };
+end
+
+local algorithms = {
+ HS256 = new_hmac_algorithm("HS256", hashes.hmac_sha256);
+ ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify);
+};
+
+local function new_signer(algorithm, key_input)
+ local impl = assert(algorithms[algorithm], "Unknown JWT algorithm: "..algorithm);
+ local key = (impl.load_private_key or impl.load_key)(key_input);
+ local sign = impl.sign;
+ return function (payload)
+ return sign(key, payload);
+ end
+end
+
+local function new_verifier(algorithm, key_input)
+ local impl = assert(algorithms[algorithm], "Unknown JWT algorithm: "..algorithm);
+ local key = (impl.load_public_key or impl.load_key)(key_input);
+ local verify = impl.verify;
+ return function (token)
+ return verify(key, token);
end
- return true, payload;
end
return {
- sign = sign;
- verify = verify;
+ new_signer = new_signer;
+ new_verifier = new_verifier;
+ -- Deprecated
+ sign = algorithms.HS256.sign;
+ verify = algorithms.HS256.verify;
};