Skip to content

Commit 06f461a

Browse files
committed
Added test vectors from the IETF JOSE Cookbook for HMAC, RSA, and EC.
1 parent 4797f7f commit 06f461a

File tree

8 files changed

+239
-1
lines changed

8 files changed

+239
-1
lines changed

tests/keys/__init__.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import json
2+
import os
3+
4+
from jwt.utils import base64url_decode
5+
6+
from tests.utils import ensure_bytes, int_from_bytes
7+
8+
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
9+
10+
11+
def decode_value(val):
12+
decoded = base64url_decode(ensure_bytes(val))
13+
return int_from_bytes(decoded, 'big')
14+
15+
16+
def load_hmac_key():
17+
with open(os.path.join(BASE_PATH, 'jwk_hmac.json'), 'r') as infile:
18+
keyobj = json.load(infile)
19+
20+
return base64url_decode(ensure_bytes(keyobj['k']))
21+
22+
try:
23+
from cryptography.hazmat.primitives.asymmetric import rsa
24+
from cryptography.hazmat.primitives.asymmetric import ec
25+
from cryptography.hazmat.backends import default_backend
26+
27+
has_crypto = True
28+
except ImportError:
29+
has_crypto = False
30+
31+
if has_crypto:
32+
def load_rsa_key():
33+
with open(os.path.join(BASE_PATH, 'jwk_rsa_key.json'), 'r') as infile:
34+
keyobj = json.load(infile)
35+
36+
return rsa.RSAPrivateNumbers(
37+
p=decode_value(keyobj['p']),
38+
q=decode_value(keyobj['q']),
39+
d=decode_value(keyobj['d']),
40+
dmp1=decode_value(keyobj['dp']),
41+
dmq1=decode_value(keyobj['dq']),
42+
iqmp=decode_value(keyobj['qi']),
43+
public_numbers=load_rsa_pub_key().public_numbers()
44+
).private_key(default_backend())
45+
46+
def load_rsa_pub_key():
47+
with open(os.path.join(BASE_PATH, 'jwk_rsa_pub.json'), 'r') as infile:
48+
keyobj = json.load(infile)
49+
50+
return rsa.RSAPublicNumbers(
51+
n=decode_value(keyobj['n']),
52+
e=decode_value(keyobj['e'])
53+
).public_key(default_backend())
54+
55+
def load_ec_key():
56+
with open(os.path.join(BASE_PATH, 'jwk_ec_key.json'), 'r') as infile:
57+
keyobj = json.load(infile)
58+
59+
return ec.EllipticCurvePrivateNumbers(
60+
private_value=decode_value(keyobj['d']),
61+
public_numbers=load_ec_pub_key().public_numbers()
62+
)
63+
64+
def load_ec_pub_key():
65+
with open(os.path.join(BASE_PATH, 'jwk_ec_pub.json'), 'r') as infile:
66+
keyobj = json.load(infile)
67+
68+
return ec.EllipticCurvePublicNumbers(
69+
x=decode_value(keyobj['x']),
70+
y=decode_value(keyobj['y']),
71+
curve=ec.SECP521R1()
72+
).public_key(default_backend())

tests/keys/jwk_ec_key.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"kty": "EC",
3+
"kid": "bilbo.baggins@hobbiton.example",
4+
"use": "sig",
5+
"crv": "P-521",
6+
"x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
7+
"y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1",
8+
"d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"
9+
}

tests/keys/jwk_ec_pub.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"kty": "EC",
3+
"kid": "bilbo.baggins@hobbiton.example",
4+
"use": "sig",
5+
"crv": "P-521",
6+
"x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
7+
"y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
8+
}

tests/keys/jwk_hmac.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"kty": "oct",
3+
"kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037",
4+
"use": "sig",
5+
"alg": "HS256",
6+
"k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
7+
}

tests/keys/jwk_rsa_key.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"kty": "RSA",
3+
"kid": "bilbo.baggins@hobbiton.example",
4+
"use": "sig",
5+
"n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw",
6+
"e": "AQAB",
7+
"d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ",
8+
"p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nRaO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmGpeNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0k",
9+
"q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc",
10+
"dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn-RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX59ehik",
11+
"dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pErAMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdKT1cYF8",
12+
"qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-NZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDhjJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpPz8aaI4"
13+
}

tests/keys/jwk_rsa_pub.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"kty": "RSA",
3+
"kid": "bilbo.baggins@hobbiton.example",
4+
"use": "sig",
5+
"n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw",
6+
"e": "AQAB"
7+
}

tests/test_algorithms.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
from jwt.algorithms import Algorithm, HMACAlgorithm, NoneAlgorithm
44
from jwt.exceptions import InvalidKeyError
5+
from jwt.utils import base64url_decode
56

67
import pytest
78

9+
from .keys import load_hmac_key
810
from .utils import ensure_bytes, ensure_unicode, key_path
911

1012
try:
1113
from jwt.algorithms import RSAAlgorithm, ECAlgorithm, RSAPSSAlgorithm
12-
14+
from .keys import load_rsa_pub_key, load_ec_pub_key
1315
has_crypto = True
1416
except ImportError:
1517
has_crypto = False
@@ -257,3 +259,102 @@ def test_rsa_pss_verify_should_return_true_if_signature_valid(self):
257259

258260
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
259261
assert result
262+
263+
264+
class TestAlgorithmsCookbook:
265+
"""
266+
These test vectors were taken from IETF JOSE Cookbook Draft
267+
(https://www.ietf.org/id/draft-ietf-jose-cookbook-08.txt)
268+
"""
269+
270+
def test_hmac_verify_should_return_true_for_test_vector(self):
271+
signing_input = ensure_bytes(
272+
'eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZ'
273+
'jMxNGJjNzAzNyJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ'
274+
'29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIG'
275+
'lmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmc'
276+
'gd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4'
277+
)
278+
279+
signature = base64url_decode(ensure_bytes(
280+
's0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0'
281+
))
282+
283+
algo = HMACAlgorithm(HMACAlgorithm.SHA256)
284+
key = algo.prepare_key(load_hmac_key())
285+
286+
result = algo.verify(signing_input, key, signature)
287+
assert result
288+
289+
@pytest.mark.skipif(not has_crypto, reason='Not supported without cryptography library')
290+
def test_rsa_verify_should_return_true_for_test_vector(self):
291+
signing_input = ensure_bytes(
292+
'eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb'
293+
'XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb'
294+
'3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS'
295+
'Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU'
296+
'geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4'
297+
)
298+
299+
signature = base64url_decode(ensure_bytes(
300+
'MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmKZop'
301+
'dHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJ'
302+
'K3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4'
303+
'QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic'
304+
'1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_fcIe8u9ipH84ogor'
305+
'ee7vjbU5y18kDquDg'
306+
))
307+
308+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
309+
key = algo.prepare_key(load_rsa_pub_key())
310+
311+
result = algo.verify(signing_input, key, signature)
312+
assert result
313+
314+
@pytest.mark.skipif(True, "I'm not 100% sure if this test is correct")
315+
@pytest.mark.skipif(not has_crypto, reason='Not supported without cryptography library')
316+
def test_rsapss_verify_should_return_true_for_test_vector(self):
317+
signing_input = ensure_bytes(
318+
'eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb'
319+
'XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb'
320+
'3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS'
321+
'Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU'
322+
'geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4'
323+
)
324+
325+
signature = base64url_decode(ensure_bytes(
326+
'cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2IpN'
327+
'6-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXUvdvW'
328+
'Xzg-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRXe8P_ij'
329+
'Q7p8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT0qI0n6ui'
330+
'P1aCN_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a6GYmJUAfmW'
331+
'jwZ6oD4ifKo8DYM-X72Eaw'
332+
))
333+
334+
algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384)
335+
key = algo.prepare_key(load_rsa_pub_key())
336+
337+
result = algo.verify(signing_input, key, signature)
338+
assert result
339+
340+
@pytest.mark.skipif(not has_crypto, reason='Not supported without cryptography library')
341+
def test_ec_verify_should_return_true_for_test_vector(self):
342+
signing_input = ensure_bytes(
343+
'eyJhbGciOiJFUzUxMiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb'
344+
'XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb'
345+
'3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS'
346+
'Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU'
347+
'geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4'
348+
)
349+
350+
signature = base64url_decode(ensure_bytes(
351+
'AE_R_YZCChjn4791jSQCrdPZCNYqHXCTZH0-JZGYNlaAjP2kqaluUIIUnC9qvbu9P'
352+
'lon7KRTzoNEuT4Va2cmL1eJAQy3mtPBu_u_sDDyYjnAMDxXPn7XrT0lw-kvAD890j'
353+
'l8e2puQens_IEKBpHABlsbEPX6sFY8OcGDqoRuBomu9xQ2'
354+
))
355+
356+
algo = ECAlgorithm(ECAlgorithm.SHA512)
357+
key = algo.prepare_key(load_ec_pub_key())
358+
359+
result = algo.verify(signing_input, key, signature)
360+
assert result

tests/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import struct
23

34
from calendar import timegm
45
from datetime import datetime
@@ -27,3 +28,23 @@ def utc_timestamp():
2728
def key_path(key_name):
2829
return os.path.join(os.path.dirname(os.path.realpath(__file__)),
2930
'keys', key_name)
31+
32+
# Borrowed from `cryptography`
33+
if hasattr(int, "from_bytes"):
34+
int_from_bytes = int.from_bytes
35+
else:
36+
def int_from_bytes(data, byteorder, signed=False):
37+
assert byteorder == 'big'
38+
assert not signed
39+
40+
if len(data) % 4 != 0:
41+
data = (b'\x00' * (4 - (len(data) % 4))) + data
42+
43+
result = 0
44+
45+
while len(data) > 0:
46+
digit, = struct.unpack('>I', data[:4])
47+
result = (result << 32) + digit
48+
data = data[4:]
49+
50+
return result

0 commit comments

Comments
 (0)