From 06f461a21102712cfc2139568531c20dc302e79c Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Mon, 18 May 2015 23:00:23 -0500 Subject: [PATCH 1/2] Added test vectors from the IETF JOSE Cookbook for HMAC, RSA, and EC. --- tests/keys/__init__.py | 72 +++++++++++++++++++++++++ tests/keys/jwk_ec_key.json | 9 ++++ tests/keys/jwk_ec_pub.json | 8 +++ tests/keys/jwk_hmac.json | 7 +++ tests/keys/jwk_rsa_key.json | 13 +++++ tests/keys/jwk_rsa_pub.json | 7 +++ tests/test_algorithms.py | 103 +++++++++++++++++++++++++++++++++++- tests/utils.py | 21 ++++++++ 8 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 tests/keys/__init__.py create mode 100644 tests/keys/jwk_ec_key.json create mode 100644 tests/keys/jwk_ec_pub.json create mode 100644 tests/keys/jwk_hmac.json create mode 100644 tests/keys/jwk_rsa_key.json create mode 100644 tests/keys/jwk_rsa_pub.json diff --git a/tests/keys/__init__.py b/tests/keys/__init__.py new file mode 100644 index 00000000..fad09f57 --- /dev/null +++ b/tests/keys/__init__.py @@ -0,0 +1,72 @@ +import json +import os + +from jwt.utils import base64url_decode + +from tests.utils import ensure_bytes, int_from_bytes + +BASE_PATH = os.path.dirname(os.path.abspath(__file__)) + + +def decode_value(val): + decoded = base64url_decode(ensure_bytes(val)) + return int_from_bytes(decoded, 'big') + + +def load_hmac_key(): + with open(os.path.join(BASE_PATH, 'jwk_hmac.json'), 'r') as infile: + keyobj = json.load(infile) + + return base64url_decode(ensure_bytes(keyobj['k'])) + +try: + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.backends import default_backend + + has_crypto = True +except ImportError: + has_crypto = False + +if has_crypto: + def load_rsa_key(): + with open(os.path.join(BASE_PATH, 'jwk_rsa_key.json'), 'r') as infile: + keyobj = json.load(infile) + + return rsa.RSAPrivateNumbers( + p=decode_value(keyobj['p']), + q=decode_value(keyobj['q']), + d=decode_value(keyobj['d']), + dmp1=decode_value(keyobj['dp']), + dmq1=decode_value(keyobj['dq']), + iqmp=decode_value(keyobj['qi']), + public_numbers=load_rsa_pub_key().public_numbers() + ).private_key(default_backend()) + + def load_rsa_pub_key(): + with open(os.path.join(BASE_PATH, 'jwk_rsa_pub.json'), 'r') as infile: + keyobj = json.load(infile) + + return rsa.RSAPublicNumbers( + n=decode_value(keyobj['n']), + e=decode_value(keyobj['e']) + ).public_key(default_backend()) + + def load_ec_key(): + with open(os.path.join(BASE_PATH, 'jwk_ec_key.json'), 'r') as infile: + keyobj = json.load(infile) + + return ec.EllipticCurvePrivateNumbers( + private_value=decode_value(keyobj['d']), + public_numbers=load_ec_pub_key().public_numbers() + ) + + def load_ec_pub_key(): + with open(os.path.join(BASE_PATH, 'jwk_ec_pub.json'), 'r') as infile: + keyobj = json.load(infile) + + return ec.EllipticCurvePublicNumbers( + x=decode_value(keyobj['x']), + y=decode_value(keyobj['y']), + curve=ec.SECP521R1() + ).public_key(default_backend()) diff --git a/tests/keys/jwk_ec_key.json b/tests/keys/jwk_ec_key.json new file mode 100644 index 00000000..a7fa999e --- /dev/null +++ b/tests/keys/jwk_ec_key.json @@ -0,0 +1,9 @@ +{ + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv": "P-521", + "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", + "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1", + "d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt" +} diff --git a/tests/keys/jwk_ec_pub.json b/tests/keys/jwk_ec_pub.json new file mode 100644 index 00000000..5259ceb7 --- /dev/null +++ b/tests/keys/jwk_ec_pub.json @@ -0,0 +1,8 @@ +{ + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv": "P-521", + "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", + "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1" +} diff --git a/tests/keys/jwk_hmac.json b/tests/keys/jwk_hmac.json new file mode 100644 index 00000000..17b0d8ac --- /dev/null +++ b/tests/keys/jwk_hmac.json @@ -0,0 +1,7 @@ +{ + "kty": "oct", + "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037", + "use": "sig", + "alg": "HS256", + "k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg" +} diff --git a/tests/keys/jwk_rsa_key.json b/tests/keys/jwk_rsa_key.json new file mode 100644 index 00000000..2db90fc1 --- /dev/null +++ b/tests/keys/jwk_rsa_key.json @@ -0,0 +1,13 @@ +{ + "kty": "RSA", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", + "e": "AQAB", + "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ", + "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nRaO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmGpeNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0k", + "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc", + "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn-RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX59ehik", + "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pErAMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdKT1cYF8", + "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-NZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDhjJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpPz8aaI4" +} diff --git a/tests/keys/jwk_rsa_pub.json b/tests/keys/jwk_rsa_pub.json new file mode 100644 index 00000000..05fc7129 --- /dev/null +++ b/tests/keys/jwk_rsa_pub.json @@ -0,0 +1,7 @@ +{ + "kty": "RSA", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", + "e": "AQAB" +} diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index b8240fa7..710a82eb 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -2,14 +2,16 @@ from jwt.algorithms import Algorithm, HMACAlgorithm, NoneAlgorithm from jwt.exceptions import InvalidKeyError +from jwt.utils import base64url_decode import pytest +from .keys import load_hmac_key from .utils import ensure_bytes, ensure_unicode, key_path try: from jwt.algorithms import RSAAlgorithm, ECAlgorithm, RSAPSSAlgorithm - + from .keys import load_rsa_pub_key, load_ec_pub_key has_crypto = True except ImportError: has_crypto = False @@ -257,3 +259,102 @@ def test_rsa_pss_verify_should_return_true_if_signature_valid(self): result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert result + + +class TestAlgorithmsCookbook: + """ + These test vectors were taken from IETF JOSE Cookbook Draft + (https://www.ietf.org/id/draft-ietf-jose-cookbook-08.txt) + """ + + def test_hmac_verify_should_return_true_for_test_vector(self): + signing_input = ensure_bytes( + 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZ' + 'jMxNGJjNzAzNyJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ' + '29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIG' + 'lmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmc' + 'gd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4' + ) + + signature = base64url_decode(ensure_bytes( + 's0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0' + )) + + algo = HMACAlgorithm(HMACAlgorithm.SHA256) + key = algo.prepare_key(load_hmac_key()) + + result = algo.verify(signing_input, key, signature) + assert result + + @pytest.mark.skipif(not has_crypto, reason='Not supported without cryptography library') + def test_rsa_verify_should_return_true_for_test_vector(self): + signing_input = ensure_bytes( + 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb' + 'XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb' + '3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS' + 'Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU' + 'geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4' + ) + + signature = base64url_decode(ensure_bytes( + 'MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmKZop' + 'dHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJ' + 'K3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4' + 'QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic' + '1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_fcIe8u9ipH84ogor' + 'ee7vjbU5y18kDquDg' + )) + + algo = RSAAlgorithm(RSAAlgorithm.SHA256) + key = algo.prepare_key(load_rsa_pub_key()) + + result = algo.verify(signing_input, key, signature) + assert result + + @pytest.mark.skipif(True, "I'm not 100% sure if this test is correct") + @pytest.mark.skipif(not has_crypto, reason='Not supported without cryptography library') + def test_rsapss_verify_should_return_true_for_test_vector(self): + signing_input = ensure_bytes( + 'eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb' + 'XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb' + '3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS' + 'Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU' + 'geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4' + ) + + signature = base64url_decode(ensure_bytes( + 'cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2IpN' + '6-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXUvdvW' + 'Xzg-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRXe8P_ij' + 'Q7p8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT0qI0n6ui' + 'P1aCN_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a6GYmJUAfmW' + 'jwZ6oD4ifKo8DYM-X72Eaw' + )) + + algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384) + key = algo.prepare_key(load_rsa_pub_key()) + + result = algo.verify(signing_input, key, signature) + assert result + + @pytest.mark.skipif(not has_crypto, reason='Not supported without cryptography library') + def test_ec_verify_should_return_true_for_test_vector(self): + signing_input = ensure_bytes( + 'eyJhbGciOiJFUzUxMiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb' + 'XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb' + '3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS' + 'Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU' + 'geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4' + ) + + signature = base64url_decode(ensure_bytes( + 'AE_R_YZCChjn4791jSQCrdPZCNYqHXCTZH0-JZGYNlaAjP2kqaluUIIUnC9qvbu9P' + 'lon7KRTzoNEuT4Va2cmL1eJAQy3mtPBu_u_sDDyYjnAMDxXPn7XrT0lw-kvAD890j' + 'l8e2puQens_IEKBpHABlsbEPX6sFY8OcGDqoRuBomu9xQ2' + )) + + algo = ECAlgorithm(ECAlgorithm.SHA512) + key = algo.prepare_key(load_ec_pub_key()) + + result = algo.verify(signing_input, key, signature) + assert result diff --git a/tests/utils.py b/tests/utils.py index bf74e785..77227023 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,5 @@ import os +import struct from calendar import timegm from datetime import datetime @@ -27,3 +28,23 @@ def utc_timestamp(): def key_path(key_name): return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'keys', key_name) + +# Borrowed from `cryptography` +if hasattr(int, "from_bytes"): + int_from_bytes = int.from_bytes +else: + def int_from_bytes(data, byteorder, signed=False): + assert byteorder == 'big' + assert not signed + + if len(data) % 4 != 0: + data = (b'\x00' * (4 - (len(data) % 4))) + data + + result = 0 + + while len(data) > 0: + digit, = struct.unpack('>I', data[:4]) + result = (result << 32) + digit + data = data[4:] + + return result From 7c870a80767b35d81d5774ba27830e8628e1d8a6 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Mon, 18 May 2015 23:13:24 -0500 Subject: [PATCH 2/2] Fixed tox.ini where flake8 wasn't running properly. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 3d871a4f..5c59e8f9 100644 --- a/tox.ini +++ b/tox.ini @@ -5,14 +5,14 @@ envlist = py{26,27,33,34}-crypto, py{27,34}-contrib_crypto, py{27,34}-nocrypto, commands = python setup.py pytest deps = - crypto,flake8: cryptography + crypto: cryptography pytest pytest-cov pytest-runner contrib_crypto: pycrypto contrib_crypto: ecdsa -[flake8] +[testenv:flake8] commands = flake8 deps =