1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
|
local cjson = require "cjson.safe"
local evp = require "resty.evp"
local hmac = require "resty.hmac"
local resty_random = require "resty.random"
local cipher = require "resty.openssl.cipher"
local _M = { _VERSION = "0.2.3" }
local mt = {
__index = _M
}
local string_rep = string.rep
local string_format = string.format
local string_sub = string.sub
local string_char = string.char
local table_concat = table.concat
local ngx_encode_base64 = ngx.encode_base64
local ngx_decode_base64 = ngx.decode_base64
local cjson_encode = cjson.encode
local cjson_decode = cjson.decode
local tostring = tostring
local error = error
local ipairs = ipairs
local type = type
local pcall = pcall
local assert = assert
local setmetatable = setmetatable
local pairs = pairs
-- define string constants to avoid string garbage collection
local str_const = {
invalid_jwt= "invalid jwt string",
regex_join_msg = "%s.%s",
regex_join_delim = "([^%s]+)",
regex_split_dot = "%.",
regex_jwt_join_str = "%s.%s.%s",
raw_underscore = "raw_",
dash = "-",
empty = "",
dotdot = "..",
table = "table",
plus = "+",
equal = "=",
underscore = "_",
slash = "/",
header = "header",
typ = "typ",
JWT = "JWT",
JWE = "JWE",
payload = "payload",
signature = "signature",
encrypted_key = "encrypted_key",
alg = "alg",
enc = "enc",
kid = "kid",
exp = "exp",
nbf = "nbf",
iss = "iss",
full_obj = "__jwt",
x5c = "x5c",
x5u = 'x5u',
HS256 = "HS256",
HS512 = "HS512",
RS256 = "RS256",
ES256 = "ES256",
ES512 = "ES512",
RS512 = "RS512",
A128CBC_HS256 = "A128CBC-HS256",
A128CBC_HS256_CIPHER_MODE = "aes-128-cbc",
A256CBC_HS512 = "A256CBC-HS512",
A256CBC_HS512_CIPHER_MODE = "aes-256-cbc",
A256GCM = "A256GCM",
A256GCM_CIPHER_MODE = "aes-256-gcm",
RSA_OAEP_256 = "RSA-OAEP-256",
DIR = "dir",
reason = "reason",
verified = "verified",
number = "number",
string = "string",
funct = "function",
boolean = "boolean",
valid = "valid",
valid_issuers = "valid_issuers",
lifetime_grace_period = "lifetime_grace_period",
require_nbf_claim = "require_nbf_claim",
require_exp_claim = "require_exp_claim",
internal_error = "internal error",
everything_awesome = "everything is awesome~ :p"
}
-- @function split string
local function split_string(str, delim)
local result = {}
local sep = string_format(str_const.regex_join_delim, delim)
for m in str:gmatch(sep) do
result[#result+1]=m
end
return result
end
-- @function is nil or boolean
-- @return true if param is nil or true or false; false otherwise
local function is_nil_or_boolean(arg_value)
if arg_value == nil then
return true
end
if type(arg_value) ~= str_const.boolean then
return false
end
return true
end
--@function get the raw part
--@param part_name
--@param jwt_obj
local function get_raw_part(part_name, jwt_obj)
local raw_part = jwt_obj[str_const.raw_underscore .. part_name]
if raw_part == nil then
local part = jwt_obj[part_name]
if part == nil then
error({reason="missing part " .. part_name})
end
raw_part = _M:jwt_encode(part)
end
return raw_part
end
--@function decrypt payload
--@param secret_key to decrypt the payload
--@param encrypted payload
--@param encryption algorithm
--@param iv which was generated while encrypting the payload
--@param aad additional authenticated data (used when gcm mode is used)
--@param auth_tag authenticated tag (used when gcm mode is used)
--@return decrypted payloaf
local function decrypt_payload(secret_key, encrypted_payload, enc, iv_in, aad, auth_tag )
local decrypted_payload, err
if enc == str_const.A128CBC_HS256 then
local aes_128_cbs_cipher = assert(cipher.new(str_const.A128CBC_HS256_CIPHER_MODE))
decrypted_payload, err= aes_128_cbs_cipher:decrypt(secret_key, iv_in, encrypted_payload)
elseif enc == str_const.A256CBC_HS512 then
local aes_256_cbs_cipher = assert(cipher.new(str_const.A256CBC_HS512_CIPHER_MODE))
decrypted_payload, err = aes_256_cbs_cipher:decrypt(secret_key, iv_in, encrypted_payload)
elseif enc == str_const.A256GCM then
local aes_256_gcm_cipher = assert(cipher.new(str_const.A256GCM_CIPHER_MODE))
decrypted_payload, err = aes_256_gcm_cipher:decrypt(secret_key, iv_in, encrypted_payload, false, aad, auth_tag)
else
return nil, "unsupported enc: " .. enc
end
if not decrypted_payload or err then
return nil, err
end
return decrypted_payload
end
-- @function encrypt payload using given secret
-- @param secret_key secret key to encrypt
-- @param message data to be encrypted. It could be lua table or string
-- @param enc algorithm to use for encryption
-- @param aad additional authenticated data (used when gcm mode is used)
local function encrypt_payload(secret_key, message, enc, aad )
if enc == str_const.A128CBC_HS256 then
local iv_rand = resty_random.bytes(16,true)
local aes_128_cbs_cipher = assert(cipher.new(str_const.A128CBC_HS256_CIPHER_MODE))
local encrypted = aes_128_cbs_cipher:encrypt(secret_key, iv_rand, message)
return encrypted, iv_rand
elseif enc == str_const.A256CBC_HS512 then
local iv_rand = resty_random.bytes(16,true)
local aes_256_cbs_cipher = assert(cipher.new(str_const.A256CBC_HS512_CIPHER_MODE))
local encrypted = aes_256_cbs_cipher:encrypt(secret_key, iv_rand, message)
return encrypted, iv_rand
elseif enc == str_const.A256GCM then
local iv_rand = resty_random.bytes(12,true) -- 96 bit IV is recommended for efficiency
local aes_256_gcm_cipher = assert(cipher.new(str_const.A256GCM_CIPHER_MODE))
local encrypted = aes_256_gcm_cipher:encrypt(secret_key, iv_rand, message, false, aad)
local auth_tag = assert(aes_256_gcm_cipher:get_aead_tag())
return encrypted, iv_rand, auth_tag
else
return nil, nil , nil, "unsupported enc: " .. enc
end
end
--@function hmac_digest : generate hmac digest based on key for input message
--@param mac_key
--@param input message
--@return hmac digest
local function hmac_digest(enc, mac_key, message)
if enc == str_const.A128CBC_HS256 then
return hmac:new(mac_key, hmac.ALGOS.SHA256):final(message)
elseif enc == str_const.A256CBC_HS512 then
return hmac:new(mac_key, hmac.ALGOS.SHA512):final(message)
else
error({reason="unsupported enc: " .. enc})
end
end
--@function dervice keys: it generates key if null based on encryption algorithm
--@param encryption type
--@param secret key
--@return secret key, mac key and encryption key
local function derive_keys(enc, secret_key)
local mac_key_len, enc_key_len = 16, 16
if enc == str_const.A256GCM then
mac_key_len, enc_key_len = 0, 32 -- we need 256 bit key
elseif enc == str_const.A128CBC_HS256 then
mac_key_len, enc_key_len = 16, 16
elseif enc == str_const.A256CBC_HS512 then
mac_key_len, enc_key_len = 32, 32
else
error({reason="unsupported payload encryption algorithm :" .. enc})
end
local secret_key_len = mac_key_len + enc_key_len
if not secret_key then
secret_key = resty_random.bytes(secret_key_len, true)
end
if #secret_key ~= secret_key_len then
error({reason="invalid pre-shared key"})
end
local mac_key = string_sub(secret_key, 1, mac_key_len)
local enc_key = string_sub(secret_key, mac_key_len + 1)
return secret_key, mac_key, enc_key
end
local function get_payload_encoder(self)
return self.payload_encoder or cjson_encode
end
local function get_payload_decoder(self)
return self.payload_decoder or cjson_decode
end
--@function parse_jwe
--@param pre-shared key
--@encoded-header
local function parse_jwe(self, preshared_key, encoded_header, encoded_encrypted_key, encoded_iv, encoded_cipher_text, encoded_auth_tag)
local header = _M:jwt_decode(encoded_header, true)
if not header then
error({reason="invalid header: " .. encoded_header})
end
local alg = header.alg
if alg ~= str_const.DIR and alg ~= str_const.RSA_OAEP_256 then
error({reason="invalid algorithm: " .. alg})
end
local key, enc_key
if alg == str_const.DIR then
if not preshared_key then
error({reason="preshared key must not be null"})
end
key, _, enc_key = derive_keys(header.enc, preshared_key)
elseif alg == str_const.RSA_OAEP_256 then
if not preshared_key then
error({reason="rsa private key must not be null"})
end
local rsa_decryptor, err = evp.RSADecryptor:new(preshared_key, nil, evp.CONST.RSA_PKCS1_OAEP_PADDING, evp.CONST.SHA256_DIGEST)
if err then
error({reason="failed to create rsa object: ".. err})
end
local secret_key, err = rsa_decryptor:decrypt(_M:jwt_decode(encoded_encrypted_key))
if err or not secret_key then
error({reason="failed to decrypt key: " .. err})
end
key, _, enc_key = derive_keys(header.enc, secret_key)
end
local cipher_text = _M:jwt_decode(encoded_cipher_text)
local iv = _M:jwt_decode(encoded_iv)
local signature_or_tag = _M:jwt_decode(encoded_auth_tag)
local basic_jwe = {
internal = {
encoded_header = encoded_header,
cipher_text = cipher_text,
key = key,
iv = iv
},
header = header,
signature = signature_or_tag
}
local payload, err = decrypt_payload(enc_key, cipher_text, header.enc, iv, encoded_header, signature_or_tag)
if err then
error({reason="failed to decrypt payload: " .. err})
else
basic_jwe.payload = get_payload_decoder(self)(payload)
basic_jwe.internal.json_payload=payload
end
return basic_jwe
end
-- @function parse_jwt
-- @param encoded header
-- @param encoded
-- @param signature
-- @return jwt table
local function parse_jwt(encoded_header, encoded_payload, signature)
local header = _M:jwt_decode(encoded_header, true)
if not header then
error({reason="invalid header: " .. encoded_header})
end
local payload = _M:jwt_decode(encoded_payload, true)
if not payload then
error({reason="invalid payload: " .. encoded_payload})
end
local basic_jwt = {
raw_header=encoded_header,
raw_payload=encoded_payload,
header=header,
payload=payload,
signature=signature
}
return basic_jwt
end
-- @function parse token - this can be JWE or JWT token
-- @param token string
-- @return jwt/jwe tables
local function parse(self, secret, token_str)
local tokens = split_string(token_str, str_const.regex_split_dot)
local num_tokens = #tokens
if num_tokens == 3 then
return parse_jwt(tokens[1], tokens[2], tokens[3])
elseif num_tokens == 4 then
return parse_jwe(self, secret, tokens[1], nil, tokens[2], tokens[3], tokens[4])
elseif num_tokens == 5 then
return parse_jwe(self, secret, tokens[1], tokens[2], tokens[3], tokens[4], tokens[5])
else
error({reason=str_const.invalid_jwt})
end
end
--@function jwt encode : it converts into base64 encoded string. if input is a table, it convets into
-- json before converting to base64 string
--@param payloaf
--@return base64 encoded payloaf
function _M.jwt_encode(self, ori, is_payload)
if type(ori) == str_const.table then
ori = is_payload and get_payload_encoder(self)(ori) or cjson_encode(ori)
end
local res = ngx_encode_base64(ori):gsub(str_const.plus, str_const.dash):gsub(str_const.slash, str_const.underscore):gsub(str_const.equal, str_const.empty)
return res
end
--@function jwt decode : decode bas64 encoded string
function _M.jwt_decode(self, b64_str, json_decode, is_payload)
b64_str = b64_str:gsub(str_const.dash, str_const.plus):gsub(str_const.underscore, str_const.slash)
local reminder = #b64_str % 4
if reminder > 0 then
b64_str = b64_str .. string_rep(str_const.equal, 4 - reminder)
end
local data = ngx_decode_base64(b64_str)
if not data then
return nil
end
if json_decode then
data = is_payload and get_payload_decoder(self)(data) or cjson_decode(data)
end
return data
end
--- Initialize the trusted certs
-- During RS256 verify, we'll make sure the
-- cert was signed by one of these
function _M.set_trusted_certs_file(self, filename)
self.trusted_certs_file = filename
end
_M.trusted_certs_file = nil
--- Set a whitelist of allowed algorithms
-- E.g., jwt:set_alg_whitelist({RS256=1,HS256=1})
--
-- @param algorithms - A table with keys for the supported algorithms
-- If the table is non-nil, during
-- verify, the alg must be in the table
function _M.set_alg_whitelist(self, algorithms)
self.alg_whitelist = algorithms
end
_M.alg_whitelist = nil
--- Returns the list of default validations that will be
--- applied upon the verification of a jwt.
function _M.get_default_validation_options(self, jwt_obj)
return {
[str_const.require_exp_claim]=jwt_obj[str_const.payload].exp ~= nil,
[str_const.require_nbf_claim]=jwt_obj[str_const.payload].nbf ~= nil
}
end
--- Set a function used to retrieve the content of x5u urls
--
-- @param retriever_function - A pointer to a function. This function should be
-- defined to accept three string parameters. First one
-- will be the value of the 'x5u' attribute. Second
-- one will be the value of the 'iss' attribute, would
-- it be defined in the jwt. Third one will be the value
-- of the 'iss' attribute, would it be defined in the jwt.
-- This function should return the matching certificate.
function _M.set_x5u_content_retriever(self, retriever_function)
if type(retriever_function) ~= str_const.funct then
error("'retriever_function' is expected to be a function", 0)
end
self.x5u_content_retriever = retriever_function
end
_M.x5u_content_retriever = nil
-- https://tools.ietf.org/html/rfc7516#appendix-B.3
-- TODO: do it in lua way
local function binlen(s)
if type(s) ~= 'string' then return end
local len = 8 * #s
return string_char(len / 0x0100000000000000 % 0x100)
.. string_char(len / 0x0001000000000000 % 0x100)
.. string_char(len / 0x0000010000000000 % 0x100)
.. string_char(len / 0x0000000100000000 % 0x100)
.. string_char(len / 0x0000000001000000 % 0x100)
.. string_char(len / 0x0000000000010000 % 0x100)
.. string_char(len / 0x0000000000000100 % 0x100)
.. string_char(len / 0x0000000000000001 % 0x100)
end
--@function sign jwe payload
--@param secret key : if used pre-shared or RSA key
--@param jwe payload
--@return jwe token
local function sign_jwe(self, secret_key, jwt_obj)
local header = jwt_obj.header
local enc = header.enc
local alg = header.alg
-- remove type
if header.typ then
header.typ = nil
end
-- TODO: implement logic for creating enc key and mac key and then encrypt key
local key, encrypted_key, mac_key, enc_key
local encoded_header = _M:jwt_encode(header)
local payload_to_encrypt = get_payload_encoder(self)(jwt_obj.payload)
if alg == str_const.DIR then
_, mac_key, enc_key = derive_keys(enc, secret_key)
encrypted_key = ""
elseif alg == str_const.RSA_OAEP_256 then
local cert, err
if secret_key:find("CERTIFICATE") then
cert, err = evp.Cert:new(secret_key)
elseif secret_key:find("PUBLIC KEY") then
cert, err = evp.PublicKey:new(secret_key)
end
if not cert then
error({reason="Decode secret is not a valid cert/public key: " .. (err and err or secret_key)})
end
local rsa_encryptor = evp.RSAEncryptor:new(cert, evp.CONST.RSA_PKCS1_OAEP_PADDING, evp.CONST.SHA256_DIGEST)
if err then
error("failed to create rsa object for encryption ".. err)
end
key, mac_key, enc_key = derive_keys(enc)
encrypted_key, err = rsa_encryptor:encrypt(key)
if err or not encrypted_key then
error({reason="failed to encrypt key " .. (err or "")})
end
else
error({reason="unsupported alg: " .. alg})
end
local cipher_text, iv, auth_tag, err = encrypt_payload(enc_key, payload_to_encrypt, enc, encoded_header)
if err then
error({reason="error while encrypting payload. Error: " .. err})
end
if not auth_tag then
local encoded_header_length = binlen(encoded_header)
local mac_input = table_concat({encoded_header , iv, cipher_text , encoded_header_length})
local mac = hmac_digest(enc, mac_key, mac_input)
auth_tag = string_sub(mac, 1, #mac/2)
end
local jwe_table = {encoded_header, _M:jwt_encode(encrypted_key), _M:jwt_encode(iv),
_M:jwt_encode(cipher_text), _M:jwt_encode(auth_tag)}
return table_concat(jwe_table, ".", 1, 5)
end
--@function get_secret_str : returns the secret if it is a string, or the result of a function
--@param either the string secret or a function that takes a string parameter and returns a string or nil
--@param jwt payload
--@return the secret as a string or as a function
local function get_secret_str(secret_or_function, jwt_obj)
if type(secret_or_function) == str_const.funct then
-- Only use with hmac algorithms
local alg = jwt_obj[str_const.header][str_const.alg]
if alg ~= str_const.HS256 and alg ~= str_const.HS512 then
error({reason="secret function can only be used with hmac alg: " .. alg})
end
-- Pull out the kid value from the header
local kid_val = jwt_obj[str_const.header][str_const.kid]
if kid_val == nil then
error({reason="secret function specified without kid in header"})
end
-- Call the function
return secret_or_function(kid_val) or error({reason="function returned nil for kid: " .. kid_val})
elseif type(secret_or_function) == str_const.string then
-- Just return the string
return secret_or_function
else
-- Throw an error
error({reason="invalid secret type (must be string or function)"})
end
end
--@function sign : create a jwt/jwe signature from jwt_object
--@param secret key
--@param jwt/jwe payload
function _M.sign(self, secret_key, jwt_obj)
-- header typ check
local typ = jwt_obj[str_const.header][str_const.typ]
-- Optional header typ check [See http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-5.1]
if typ ~= nil then
if typ ~= str_const.JWT and typ ~= str_const.JWE then
error({reason="invalid typ: " .. typ})
end
end
if typ == str_const.JWE or jwt_obj.header.enc then
return sign_jwe(self, secret_key, jwt_obj)
end
-- header alg check
local raw_header = get_raw_part(str_const.header, jwt_obj)
local raw_payload = get_raw_part(str_const.payload, jwt_obj)
local message = string_format(str_const.regex_join_msg, raw_header, raw_payload)
local alg = jwt_obj[str_const.header][str_const.alg]
local signature = ""
if alg == str_const.HS256 then
local secret_str = get_secret_str(secret_key, jwt_obj)
signature = hmac:new(secret_str, hmac.ALGOS.SHA256):final(message)
elseif alg == str_const.HS512 then
local secret_str = get_secret_str(secret_key, jwt_obj)
signature = hmac:new(secret_str, hmac.ALGOS.SHA512):final(message)
elseif alg == str_const.RS256 or alg == str_const.RS512 then
local signer, err = evp.RSASigner:new(secret_key)
if not signer then
error({reason="signer error: " .. err})
end
if alg == str_const.RS256 then
signature = signer:sign(message, evp.CONST.SHA256_DIGEST)
elseif alg == str_const.RS512 then
signature = signer:sign(message, evp.CONST.SHA512_DIGEST)
end
elseif alg == str_const.ES256 or alg == str_const.ES512 then
local signer, err = evp.ECSigner:new(secret_key)
if not signer then
error({reason="signer error: " .. err})
end
-- OpenSSL will generate a DER encoded signature that needs to be converted
local der_signature = ""
if alg == str_const.ES256 then
der_signature = signer:sign(message, evp.CONST.SHA256_DIGEST)
elseif alg == str_const.ES512 then
der_signature = signer:sign(message, evp.CONST.SHA512_DIGEST)
end
-- Perform DER to RAW signature conversion
signature, err = signer:get_raw_sig(der_signature)
if not signature then
error({reason="signature error: " .. err})
end
else
error({reason="unsupported alg: " .. alg})
end
-- return full jwt string
return string_format(str_const.regex_join_msg, message , _M:jwt_encode(signature))
end
--@function load jwt
--@param jwt string token
--@param secret
function _M.load_jwt(self, jwt_str, secret)
local success, ret = pcall(parse, self, secret, jwt_str)
if not success then
return {
valid=false,
verified=false,
reason=ret[str_const.reason] or str_const.invalid_jwt
}
end
local jwt_obj = ret
jwt_obj[str_const.verified] = false
jwt_obj[str_const.valid] = true
return jwt_obj
end
--@function verify jwe object
--@param jwt object
--@return jwt object with reason whether verified or not
local function verify_jwe_obj(jwt_obj)
if jwt_obj[str_const.header][str_const.enc] ~= str_const.A256GCM then -- tag gets authenticated during decryption
local _, mac_key, _ = derive_keys(jwt_obj.header.enc, jwt_obj.internal.key)
local encoded_header = jwt_obj.internal.encoded_header
local encoded_header_length = binlen(encoded_header)
local mac_input = table_concat({encoded_header , jwt_obj.internal.iv, jwt_obj.internal.cipher_text,
encoded_header_length})
local mac = hmac_digest(jwt_obj.header.enc, mac_key, mac_input)
local auth_tag = string_sub(mac, 1, #mac/2)
if auth_tag ~= jwt_obj.signature then
jwt_obj[str_const.reason] = "signature mismatch: " ..
tostring(jwt_obj[str_const.signature])
end
end
jwt_obj.internal = nil
jwt_obj.signature = nil
if not jwt_obj[str_const.reason] then
jwt_obj[str_const.verified] = true
jwt_obj[str_const.reason] = str_const.everything_awesome
end
return jwt_obj
end
--@function extract certificate
--@param jwt object
--@return decoded certificate
local function extract_certificate(jwt_obj, x5u_content_retriever)
local x5c = jwt_obj[str_const.header][str_const.x5c]
if x5c ~= nil and x5c[1] ~= nil then
-- TODO Might want to add support for intermediaries that we
-- don't have in our trusted chain (items 2... if present)
local cert_str = ngx_decode_base64(x5c[1])
if not cert_str then
jwt_obj[str_const.reason] = "Malformed x5c header"
end
return cert_str
end
local x5u = jwt_obj[str_const.header][str_const.x5u]
if x5u ~= nil then
-- TODO Ensure the url starts with https://
-- cf. https://tools.ietf.org/html/rfc7517#section-4.6
if x5u_content_retriever == nil then
jwt_obj[str_const.reason] = "No function has been provided to retrieve the content pointed at by the 'x5u'."
return nil
end
-- TODO Maybe validate the url against an optional list whitelisted url prefixes?
-- cf. https://news.ycombinator.com/item?id=9302394
local iss = jwt_obj[str_const.payload][str_const.iss]
local kid = jwt_obj[str_const.header][str_const.kid]
local success, ret = pcall(x5u_content_retriever, x5u, iss, kid)
if not success then
jwt_obj[str_const.reason] = "An error occured while invoking the x5u_content_retriever function."
return nil
end
return ret
end
-- TODO When both x5c and x5u are defined, the implementation should
-- ensure their content match
-- cf. https://tools.ietf.org/html/rfc7517#section-4.6
jwt_obj[str_const.reason] = "Unsupported RS256 key model"
return nil
-- TODO - Implement jwk and kid based models...
end
local function get_claim_spec_from_legacy_options(self, options)
local claim_spec = { }
local jwt_validators = require "resty.jwt-validators"
if options[str_const.valid_issuers] ~= nil then
claim_spec[str_const.iss] = jwt_validators.equals_any_of(options[str_const.valid_issuers])
end
if options[str_const.lifetime_grace_period] ~= nil then
jwt_validators.set_system_leeway(options[str_const.lifetime_grace_period] or 0)
-- If we have a leeway set, then either an NBF or an EXP should also exist requireds are added below
if options[str_const.require_nbf_claim] ~= true and options[str_const.require_exp_claim] ~= true then
claim_spec[str_const.full_obj] = jwt_validators.require_one_of({ str_const.nbf, str_const.exp })
end
end
if not is_nil_or_boolean(options[str_const.require_nbf_claim]) then
error(string.format("'%s' validation option is expected to be a boolean.", str_const.require_nbf_claim), 0)
end
if not is_nil_or_boolean(options[str_const.require_exp_claim]) then
error(string.format("'%s' validation option is expected to be a boolean.", str_const.require_exp_claim), 0)
end
if options[str_const.lifetime_grace_period] ~= nil or options[str_const.require_nbf_claim] ~= nil or options[str_const.require_exp_claim] ~= nil then
if options[str_const.require_nbf_claim] == true then
claim_spec[str_const.nbf] = jwt_validators.is_not_before()
else
claim_spec[str_const.nbf] = jwt_validators.opt_is_not_before()
end
if options[str_const.require_exp_claim] == true then
claim_spec[str_const.exp] = jwt_validators.is_not_expired()
else
claim_spec[str_const.exp] = jwt_validators.opt_is_not_expired()
end
end
return claim_spec
end
local function is_legacy_validation_options(options)
-- Validation options MUST be a table
if type(options) ~= str_const.table then
return false
end
-- Validation options MUST have at least one of these, and must ONLY have these
local legacy_options = { }
legacy_options[str_const.valid_issuers]=1
legacy_options[str_const.lifetime_grace_period]=1
legacy_options[str_const.require_nbf_claim]=1
legacy_options[str_const.require_exp_claim]=1
local is_legacy = false
for k in pairs(options) do
if legacy_options[k] ~= nil then
is_legacy = true
else
return false
end
end
return is_legacy
end
-- Validates the claims for the given (parsed) object
local function validate_claims(self, jwt_obj, ...)
local claim_specs = {...}
if #claim_specs == 0 then
table.insert(claim_specs, _M:get_default_validation_options(jwt_obj))
end
if jwt_obj[str_const.reason] ~= nil then
return false
end
-- Encode the current jwt_obj and use it when calling the individual validation functions
local jwt_json = cjson_encode(jwt_obj)
-- Validate all our specs
for _, claim_spec in ipairs(claim_specs) do
if is_legacy_validation_options(claim_spec) then
claim_spec = get_claim_spec_from_legacy_options(self, claim_spec)
end
for claim, fx in pairs(claim_spec) do
if type(fx) ~= str_const.funct then
error("Claim spec value must be a function - see jwt-validators.lua for helper functions", 0)
end
local val = claim == str_const.full_obj and cjson_decode(jwt_json) or jwt_obj.payload[claim]
local success, ret = pcall(fx, val, claim, jwt_json)
if not success then
jwt_obj[str_const.reason] = ret.reason or string.gsub(ret, "^.-:%d-: ", "")
return false
elseif ret == false then
jwt_obj[str_const.reason] = string.format("Claim '%s' ('%s') returned failure", claim, val)
return false
end
end
end
-- Everything was good
return true
end
--@function verify jwt object
--@param secret
--@param jwt_object
--@leeway
--@return verified jwt payload or jwt object with error code
function _M.verify_jwt_obj(self, secret, jwt_obj, ...)
if not jwt_obj.valid then
return jwt_obj
end
-- validate any claims that have been passed in
if not validate_claims(self, jwt_obj, ...) then
return jwt_obj
end
-- if jwe, invoked verify jwe
if jwt_obj[str_const.header][str_const.enc] then
return verify_jwe_obj(jwt_obj)
end
local alg = jwt_obj[str_const.header][str_const.alg]
local jwt_str = string_format(str_const.regex_jwt_join_str, jwt_obj.raw_header , jwt_obj.raw_payload , jwt_obj.signature)
if self.alg_whitelist ~= nil then
if self.alg_whitelist[alg] == nil then
return {verified=false, reason="whitelist unsupported alg: " .. alg}
end
end
if alg == str_const.HS256 or alg == str_const.HS512 then
local success, ret = pcall(_M.sign, self, secret, jwt_obj)
if not success then
-- syntax check
jwt_obj[str_const.reason] = ret[str_const.reason] or str_const.internal_error
elseif jwt_str ~= ret then
-- signature check
jwt_obj[str_const.reason] = "signature mismatch: " .. jwt_obj[str_const.signature]
end
elseif alg == str_const.RS256 or alg == str_const.RS512 or alg == str_const.ES256 or alg == str_const.ES512 then
local cert, err
if self.trusted_certs_file ~= nil then
local cert_str = extract_certificate(jwt_obj, self.x5u_content_retriever)
if not cert_str then
return jwt_obj
end
cert, err = evp.Cert:new(cert_str)
if not cert then
jwt_obj[str_const.reason] = "Unable to extract signing cert from JWT: " .. err
return jwt_obj
end
-- Try validating against trusted CA's, then a cert passed as secret
local trusted = cert:verify_trust(self.trusted_certs_file)
if not trusted then
jwt_obj[str_const.reason] = "Cert used to sign the JWT isn't trusted: " .. err
return jwt_obj
end
elseif secret ~= nil then
if secret:find("CERTIFICATE") then
cert, err = evp.Cert:new(secret)
elseif secret:find("PUBLIC KEY") then
cert, err = evp.PublicKey:new(secret)
end
if not cert then
jwt_obj[str_const.reason] = "Decode secret is not a valid cert/public key"
return jwt_obj
end
else
jwt_obj[str_const.reason] = "No trusted certs loaded"
return jwt_obj
end
local verifier = ''
if alg == str_const.RS256 or alg == str_const.RS512 then
verifier = evp.RSAVerifier:new(cert)
elseif alg == str_const.ES256 or alg == str_const.ES512 then
verifier = evp.ECVerifier:new(cert)
end
if not verifier then
-- Internal error case, should not happen...
jwt_obj[str_const.reason] = "Failed to build verifier " .. err
return jwt_obj
end
-- assemble jwt parts
local raw_header = get_raw_part(str_const.header, jwt_obj)
local raw_payload = get_raw_part(str_const.payload, jwt_obj)
local message =string_format(str_const.regex_join_msg, raw_header , raw_payload)
local sig = _M:jwt_decode(jwt_obj[str_const.signature], false)
if not sig then
jwt_obj[str_const.reason] = "Wrongly encoded signature"
return jwt_obj
end
local verified = false
err = "verify error: reason unknown"
if alg == str_const.RS256 or alg == str_const.ES256 then
verified, err = verifier:verify(message, sig, evp.CONST.SHA256_DIGEST)
elseif alg == str_const.RS512 or alg == str_const.ES512 then
verified, err = verifier:verify(message, sig, evp.CONST.SHA512_DIGEST)
end
if not verified then
jwt_obj[str_const.reason] = err
end
else
jwt_obj[str_const.reason] = "Unsupported algorithm " .. alg
end
if not jwt_obj[str_const.reason] then
jwt_obj[str_const.verified] = true
jwt_obj[str_const.reason] = str_const.everything_awesome
end
return jwt_obj
end
function _M.verify(self, secret, jwt_str, ...)
local jwt_obj = _M.load_jwt(self, jwt_str, secret)
if not jwt_obj.valid then
return {verified=false, reason=jwt_obj[str_const.reason]}
end
return _M.verify_jwt_obj(self, secret, jwt_obj, ...)
end
function _M.set_payload_encoder(self, encoder)
if type(encoder) ~= "function" then
error({reason="payload encoder must be function"})
end
self.payload_encoder = encoder
end
function _M.set_payload_decoder(self, decoder)
if type(decoder) ~= "function" then
error({reason="payload decoder must be function"})
end
self.payload_decoder= decoder
end
function _M.new()
return setmetatable({}, mt)
end
return _M
|