diff options
author | Simo Sorce <simo@redhat.com> | 2015-03-04 21:22:05 -0500 |
---|---|---|
committer | Simo Sorce <simo@redhat.com> | 2015-03-08 16:19:37 -0400 |
commit | c48d7b2e49e779f0593e98dddb9f4aa11d5beb6c (patch) | |
tree | 00c2070e093de27cbd0d993adb2c06444855330a | |
parent | 9a36f12f15552467ccdaa855aa036f73a7305396 (diff) | |
download | jwcrypto-c48d7b2e49e779f0593e98dddb9f4aa11d5beb6c.tar.gz jwcrypto-c48d7b2e49e779f0593e98dddb9f4aa11d5beb6c.tar.xz jwcrypto-c48d7b2e49e779f0593e98dddb9f4aa11d5beb6c.zip |
Add JWS implementation
Implements:
draft-ietf-jose-json-web-signature-41
plus Tests
Signed-off-by: Simo Sorce <simo@redhat.com>
-rw-r--r-- | jwcrypto/common.py | 8 | ||||
-rw-r--r-- | jwcrypto/jws.py | 461 | ||||
-rw-r--r-- | jwcrypto/tests.py | 260 | ||||
-rw-r--r-- | requirements.txt | 2 |
4 files changed, 729 insertions, 2 deletions
diff --git a/jwcrypto/common.py b/jwcrypto/common.py index e348b44..57697ca 100644 --- a/jwcrypto/common.py +++ b/jwcrypto/common.py @@ -20,3 +20,11 @@ def base64url_decode(payload): elif l != 0: raise ValueError('Invalid base64 string') return urlsafe_b64decode(payload) + + +class InvalidJWAAlgorithm(Exception): + def __init__(self, message=None): + msg = 'Invalid JWS Algorithm name' + if message: + msg += ' (%s)' % message + super(InvalidJWAAlgorithm, self).__init__(msg) diff --git a/jwcrypto/jws.py b/jwcrypto/jws.py new file mode 100644 index 0000000..63174d6 --- /dev/null +++ b/jwcrypto/jws.py @@ -0,0 +1,461 @@ +# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, hmac +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import utils as ec_utils +from cryptography.exceptions import InvalidSignature +from jwcrypto.common import base64url_encode, base64url_decode +from jwcrypto.common import InvalidJWAAlgorithm +from jwcrypto.jwk import JWK +import json + + +# draft-ietf-jose-json-web-signature-41 - 9.1 +# name: (description, supported?) +JWSHeaderRegistry = {'alg': ('Algorithm', True), + 'jku': ('JWK Set URL', False), + 'jwk': ('JSON Web Key', False), + 'kid': ('Key ID', True), + 'x5u': ('X.509 URL', False), + 'x5c': ('X.509 Certificate Chain', False), + 'x5t': ('X.509 Certificate SHA-1 Thumbprint', False), + 'x5t#S256': ('X.509 Certificate SHA-256 Thumbprint', + False), + 'typ': ('Type', True), + 'cty': ('Content Type', True), + 'crit': ('Critical', True)} + + +class InvalidJWSSignature(Exception): + def __init__(self, message=None, exception=None): + msg = None + if message: + msg = message + else: + msg = 'Unknown Signature Verification Failure' + if exception: + msg += ' {%s}' % str(exception) + super(InvalidJWSSignature, self).__init__(msg) + + +class InvalidJWSObject(Exception): + def __init__(self, message=None, exception=None): + msg = 'Invalid JWS Object' + if message: + msg += ' [%s]' % message + if exception: + msg += ' {%s}' % str(exception) + super(InvalidJWSObject, self).__init__(msg) + + +class InvalidJWSOperation(Exception): + def __init__(self, message=None, exception=None): + msg = None + if message: + msg = message + else: + msg = 'Unknown Operation Failure' + if exception: + msg += ' {%s}' % str(exception) + super(InvalidJWSOperation, self).__init__(msg) + + +class _raw_jws(object): + + def sign(self, key, payload): + raise NotImplementedError + + def verify(self, key, payload, signature): + raise NotImplementedError + + +class _raw_hmac(_raw_jws): + + def __init__(self, hashfn): + self.backend = default_backend() + self.hashfn = hashfn + + def _hmac_setup(self, key, payload): + h = hmac.HMAC(key, self.hashfn, backend=self.backend) + h.update(payload) + return h + + def sign(self, key, payload): + skey = base64url_decode(key.sign_key()) + h = self._hmac_setup(skey, payload) + return h.finalize() + + def verify(self, key, payload, signature): + vkey = base64url_decode(key.verify_key()) + h = self._hmac_setup(vkey, payload) + try: + h.verify(signature) + except InvalidSignature, e: + raise InvalidJWSSignature(exception=e) + + +class _raw_rsa(_raw_jws): + def __init__(self, padfn, hashfn): + self.padfn = padfn + self.hashfn = hashfn + + def sign(self, key, payload): + skey = key.sign_key() + signer = skey.signer(self.padfn, self.hashfn) + signer.update(payload) + return signer.finalize() + + def verify(self, key, payload, signature): + pkey = key.verify_key() + verifier = pkey.verifier(signature, self.padfn, self.hashfn) + verifier.update(payload) + verifier.verify() + + +class _raw_ec(_raw_jws): + def __init__(self, curve, hashfn): + self.curve = curve + self.hashfn = hashfn + + def encode_int(self, n, l): + e = hex(n).rstrip("L").lstrip("0x") + L = (l + 7) / 8 # number of bytes rounded up + e = '0' * (L * 2 - len(e)) + e # pad as necessary + return e.decode('hex') + + def sign(self, key, payload): + skey = key.sign_key(self.curve) + signer = skey.signer(ec.ECDSA(self.hashfn)) + signer.update(payload) + signature = signer.finalize() + r, s = ec_utils.decode_rfc6979_signature(signature) + l = key.get_curve(self.curve).key_size + return self.encode_int(r, l) + self.encode_int(s, l) + + def verify(self, key, payload, signature): + pkey = key.verify_key(self.curve) + r = signature[:len(signature)/2] + s = signature[len(signature)/2:] + enc_signature = ec_utils.encode_rfc6979_signature( + int(r.encode('hex'), 16), int(s.encode('hex'), 16)) + verifier = pkey.verifier(enc_signature, ec.ECDSA(self.hashfn)) + verifier.update(payload) + verifier.verify() + + +class _raw_none(_raw_jws): + + def sign(self, key, payload): + return '' + + def verify(self, key, payload, signature): + if signature != '': + raise InvalidJWSSignature('The "none" signature must be the ' + 'empty string') + + +class JWSCore(object): + + def __init__(self, alg, key, header, payload): + """ Generates or verifies JWS tokens. + See draft-ietf-jose-json-web-signature-41 + + :param alg: The algorithm used to produce the signature. + See draft-ietf-jose-json-web-algorithms-24 + + + :param key: A JWK key of appropriate type for the "alg" + provided in the 'protected' json string. + See draft-ietf-jose-json-web-key-41 + + :param header: A JSON string representing the protected header. + + :param payload(bytes): An arbitrary value + + :raises: InvalidJWAAlgorithm + """ + self.alg = alg + self.engine = self._jwa(alg) + if not isinstance(key, JWK): + raise ValueError('key is not a JWK object') + self.key = key + + self.protected = base64url_encode(unicode(header, 'utf-8')) + self.payload = base64url_encode(payload) + + def _jwa_HS256(self): + return _raw_hmac(hashes.SHA256()) + + def _jwa_HS384(self): + return _raw_hmac(hashes.SHA384()) + + def _jwa_HS512(self): + return _raw_hmac(hashes.SHA512()) + + def _jwa_RS256(self): + return _raw_rsa(padding.PKCS1v15(), hashes.SHA256()) + + def _jwa_RS384(self): + return _raw_rsa(padding.PKCS1v15(), hashes.SHA384()) + + def _jwa_RS512(self): + return _raw_rsa(padding.PKCS1v15(), hashes.SHA512()) + + def _jwa_ES256(self): + return _raw_ec('P-256', hashes.SHA256()) + + def _jwa_ES384(self): + return _raw_ec('P-384', hashes.SHA384()) + + def _jwa_ES512(self): + return _raw_ec('P-521', hashes.SHA512()) + + def _jwa_PS256(self): + return _raw_rsa(padding.PSS(padding.MGF1(hashes.SHA256()), + padding.PSS.MAX_LENGTH), + hashes.SHA256()) + + def _jwa_PS384(self): + return _raw_rsa(padding.PSS(padding.MGF1(hashes.SHA384()), + padding.PSS.MAX_LENGTH), + hashes.SHA384()) + + def _jwa_PS512(self): + return _raw_rsa(padding.PSS(padding.MGF1(hashes.SHA512()), + padding.PSS.MAX_LENGTH), + hashes.SHA512()) + + def _jwa_none(self): + return _raw_none() + + def _jwa(self, name): + attr = '_jwa_%s' % name + try: + return getattr(self, attr)() + except (KeyError, AttributeError): + raise InvalidJWAAlgorithm() + + def sign(self): + signing_input = str.encode('.'.join([self.protected, self.payload])) + signature = self.engine.sign(self.key, signing_input) + return {'protected': self.protected, + 'payload': self.payload, + 'signature': base64url_encode(signature)} + + def verify(self, signature): + try: + signing_input = '.'.join([self.protected, self.payload]) + self.engine.verify(self.key, signing_input, signature) + except Exception, e: # pylint: disable=broad-except + raise InvalidJWSSignature('Verification failed', e) + return True + + +class JWS(object): + def __init__(self, payload=None): + """ Generates or verifies Generic JWS tokens. + See draft-ietf-jose-json-web-signature-41 + + :param payload(bytes): An arbitrary value + """ + self.objects = dict() + if payload: + self.objects['payload'] = payload + + def check_crit(self, crit): + for k in crit: + if k not in JWSHeaderRegistry: + raise InvalidJWSSignature('Unknown critical header: ' + '"%s"' % k) + else: + if not JWSHeaderRegistry[k][1]: + raise InvalidJWSSignature('Unsupported critical ' + 'header: "%s"' % k) + + # TODO: support selecting key with 'kid' and passing in multiple keys + def verify(self, alg, key, payload, signature, protected, header=None): + # verify it is a valid JSON object and keep a decode copy + p = json.loads(protected) + # merge heders, and verify there are no duplicates + if header: + h = json.loads(header) + for k in p.keys(): + if k in h: + raise InvalidJWSSignature('Duplicate header: "%s"' % k) + p.update(header) + # verify critical headers + # TODO: allow caller to specify list of headers it understands + if 'crit' in p: + self.check_crit(p['crit']) + # check 'alg' is present + if 'alg' not in p: + raise InvalidJWSSignature('No "alg" in protected header') + if alg: + if alg != p['alg']: + raise InvalidJWSSignature('"alg" mismatch, requested ' + '"%s", found "%s"' % (alg, + p['alg'])) + a = alg + else: + a = p['alg'] + + # the following will verify the "alg" iss upported and the signature + # verifies + S = JWSCore(a, key, protected, payload) + S.verify(signature) + + def deserialize(self, raw_jws, key=None, alg=None): + """ Destroys any current status and tries to import the raw + JWS provided. + """ + self.objects = dict() + o = dict() + try: + try: + djws = json.loads(raw_jws) + o['payload'] = base64url_decode(str(djws['payload'])) + if 'signatures' in djws: + o['signatures'] = list() + for s in djws['signatures']: + os = dict() + os['protected'] = base64url_decode(str(s['protected'])) + os['signature'] = base64url_decode(str(s['signature'])) + if 'header' in s: + os['header'] = json.dumps(s['header']) + try: + self.verify(alg, key, o['payload'], + os['signature'], os['protected'], + os.get('header', None)) + os['valid'] = True + except Exception: # pylint: disable=broad-except + os['valid'] = False + o['signatures'].append(os) + else: + o['protected'] = base64url_decode(str(djws['protected'])) + o['protected'] = base64url_decode(str(djws['signature'])) + if 'header' in djws: + o['header'] = json.dumps(djws['header']) + try: + self.verify(alg, key, o['payload'], + o['signature'], o['protected'], + o.get('header', None)) + o['valid'] = True + except Exception: # pylint: disable=broad-except + o['valid'] = False + + except ValueError: + c = raw_jws.split('.') + if len(c) != 3: + raise InvalidJWSObject() + o['protected'] = base64url_decode(str(c[0])) + o['payload'] = base64url_decode(str(c[1])) + o['signature'] = base64url_decode(str(c[2])) + try: + self.verify(alg, key, o['payload'], o['signature'], + o['protected'], None) + o['valid'] = True + except Exception: # pylint: disable=broad-except + o['valid'] = False + + except Exception, e: # pylint: disable=broad-except + raise InvalidJWSObject('Invalid format', e) + + self.objects = o + + def add_signature(self, key, alg=None, protected=None, header=None): + if not self.objects.get('payload', None): + raise InvalidJWSObject('Missing Payload') + + o = dict() + p = None + if alg is None and protected is None: + raise ValueError('"alg" not specified') + if protected: + p = json.loads(protected) + else: + p = {'alg': alg} + protected = json.dumps(p) + if alg and alg != p['alg']: + raise ValueError('"alg" value mismatch, specified "alg" does ' + 'not match "protected" header value') + a = alg if alg else p['alg'] + # TODO: allow caller to specify list of headers it understands + if 'crit' in p: + self.check_crit(p['crit']) + + if header: + h = json.loads(header) + for k in p.keys(): + if k in h: + raise ValueError('Duplicate header: "%s"' % k) + + S = JWSCore(a, key, protected, self.objects['payload']) + sig = S.sign() + + o['signature'] = base64url_decode(sig['signature']) + o['protected'] = protected + if header: + o['header'] = h + o['valid'] = True + + if 'signatures' in self.objects: + self.objects['signatures'].append(o) + elif 'signature' in self.objects: + self.objects['signatures'] = list() + n = dict() + n['signature'] = self.objects['signature'] + del self.objects['signature'] + n['protected'] = self.objects['protected'] + del self.objects['protected'] + if 'header' in self.objects: + n['header'] = self.objects['header'] + del self.objects['header'] + if 'valid' in self.objects: + n['valid'] = self.objects['valid'] + del self.objects['valid'] + self.objects['signatures'].append(n) + self.objects['signatures'].append(o) + else: + self.objects.update(o) + + def serialize(self, compact=False): + + if compact: + if 'signatures' in self.objects: + raise InvalidJWSOperation("Can't use compact encoding with " + "multiple signatures") + if 'signature' not in self.objects: + raise InvalidJWSSignature("No available signature") + if not self.objects.get('valid', False): + raise InvalidJWSSignature("No valid signature found") + return '.'.join([base64url_encode(self.objects['protected']), + base64url_encode(self.objects['payload']), + base64url_encode(self.objects['signature'])]) + else: + obj = self.objects + if 'signature' in obj: + if not obj.get('valid', False): + raise InvalidJWSSignature("No valid signature found") + sig = {'payload': base64url_encode(obj['payload']), + 'protected': base64url_encode(obj['protected']), + 'signature': base64url_encode(obj['signature'])} + if 'header' in obj: + sig['header'] = obj['header'] + elif 'signatures' in obj: + sig = {'payload': base64url_encode(obj['payload']), + 'signatures': list()} + for o in obj['signatures']: + if not o.get('valid', False): + continue + s = {'protected': base64url_encode(o['protected']), + 'signature': base64url_encode(o['signature'])} + if 'header' in o: + s['header'] = o['header'] + sig['signatures'].append(s) + if len(sig['signatures']) == 0: + raise InvalidJWSSignature("No valid signature found") + else: + raise InvalidJWSSignature("No available signature") + return json.dumps(sig) diff --git a/jwcrypto/tests.py b/jwcrypto/tests.py index 8e6c14c..881dfd7 100644 --- a/jwcrypto/tests.py +++ b/jwcrypto/tests.py @@ -1,6 +1,8 @@ # Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file +from jwcrypto.common import base64url_decode from jwcrypto import jwk +from jwcrypto import jws import json import unittest @@ -157,15 +159,17 @@ RSAPrivateKey = {"kty": "RSA", class TestJWK(unittest.TestCase): - def test_create(self): + def test_create_pubKeys(self): keylist = PublicKeys['keys'] for key in keylist: _ = jwk.JWK(**key) # pylint: disable=star-args + def test_create_priKeys(self): keylist = PrivateKeys['keys'] for key in keylist: _ = jwk.JWK(**key) # pylint: disable=star-args + def test_create_symKeys(self): keylist = SymmetricKeys['keys'] for key in keylist: jwkey = jwk.JWK(**key) # pylint: disable=star-args @@ -176,3 +180,257 @@ class TestJWK(unittest.TestCase): _ = jwk.JWK(**Useofx5c) # pylint: disable=star-args _ = jwk.JWK(**RSAPrivateKey) # pylint: disable=star-args + + +# draft-ietf-jose-json-web-signature-41 - A.1 +A1_protected = \ + [123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, + 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125] +A1_payload = \ + [123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, + 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, + 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, + 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, + 111, 116, 34, 58, 116, 114, 117, 101, 125] +A1_signature = \ + [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, + 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, + 132, 141, 121] +A1_example = {'key': SymmetricKeys['keys'][1], + 'alg': 'HS256', + 'protected': ''.join([chr(x) for x in A1_protected]), + 'payload': ''.join([chr(x) for x in A1_payload]), + 'signature': ''.join([chr(x) for x in A1_signature])} + +# draft-ietf-jose-json-web-signature-41 - A.2 +A2_protected = \ + [123, 34, 97, 108, 103, 34, 58, 34, 82, 83, 50, 53, 54, 34, 125] +A2_payload = A1_payload +A2_key = \ + {"kty": "RSA", + "n": "ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx" + "HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs" + "D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH" + "SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV" + "MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8" + "NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", + "e": "AQAB", + "d": "Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I" + "jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0" + "BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn" + "439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT" + "CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh" + "BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ", + "p": "4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdi" + "YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG" + "BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc", + "q": "uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxa" + "ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA" + "-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc", + "dp": "BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3Q" + "CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb" + "34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0", + "dq": "h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa" + "7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky" + "NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU", + "qi": "IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2o" + "y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU" + "W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U"} +A2_signature = \ + [112, 46, 33, 137, 67, 232, 143, 209, 30, 181, 216, 45, 191, 120, 69, + 243, 65, 6, 174, 27, 129, 255, 247, 115, 17, 22, 173, 209, 113, 125, + 131, 101, 109, 66, 10, 253, 60, 150, 238, 221, 115, 162, 102, 62, 81, + 102, 104, 123, 0, 11, 135, 34, 110, 1, 135, 237, 16, 115, 249, 69, + 229, 130, 173, 252, 239, 22, 216, 90, 121, 142, 232, 198, 109, 219, + 61, 184, 151, 91, 23, 208, 148, 2, 190, 237, 213, 217, 217, 112, 7, + 16, 141, 178, 129, 96, 213, 248, 4, 12, 167, 68, 87, 98, 184, 31, + 190, 127, 249, 217, 46, 10, 231, 111, 36, 242, 91, 51, 187, 230, 244, + 74, 230, 30, 177, 4, 10, 203, 32, 4, 77, 62, 249, 18, 142, 212, 1, + 48, 121, 91, 212, 189, 59, 65, 238, 202, 208, 102, 171, 101, 25, 129, + 253, 228, 141, 247, 127, 55, 45, 195, 139, 159, 175, 221, 59, 239, + 177, 139, 93, 163, 204, 60, 46, 176, 47, 158, 58, 65, 214, 18, 202, + 173, 21, 145, 18, 115, 160, 95, 35, 185, 232, 56, 250, 175, 132, 157, + 105, 132, 41, 239, 90, 30, 136, 121, 130, 54, 195, 212, 14, 96, 69, + 34, 165, 68, 200, 242, 122, 122, 45, 184, 6, 99, 209, 108, 247, 202, + 234, 86, 222, 64, 92, 178, 33, 90, 69, 178, 194, 85, 102, 181, 90, + 193, 167, 72, 160, 112, 223, 200, 163, 42, 70, 149, 67, 208, 25, 238, + 251, 71] +A2_example = {'key': A2_key, + 'alg': 'RS256', + 'protected': ''.join([chr(x) for x in A2_protected]), + 'payload': ''.join([chr(x) for x in A2_payload]), + 'signature': ''.join([chr(x) for x in A2_signature])} + +# draft-ietf-jose-json-web-signature-41 - A.3 +A3_protected = \ + [123, 34, 97, 108, 103, 34, 58, 34, 69, 83, 50, 53, 54, 34, 125] +A3_payload = A2_payload +A3_key = \ + {"kty": "EC", + "crv": "P-256", + "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", + "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", + "d": "jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI"} +A3_signature = \ + [14, 209, 33, 83, 121, 99, 108, 72, 60, 47, 127, 21, 88, + 7, 212, 2, 163, 178, 40, 3, 58, 249, 124, 126, 23, 129, + 154, 195, 22, 158, 166, 101] + \ + [197, 10, 7, 211, 140, 60, 112, 229, 216, 241, 45, 175, + 8, 74, 84, 128, 166, 101, 144, 197, 242, 147, 80, 154, + 143, 63, 127, 138, 131, 163, 84, 213] +A3_example = {'key': A3_key, + 'alg': 'ES256', + 'protected': ''.join([chr(x) for x in A3_protected]), + 'payload': ''.join([chr(x) for x in A3_payload]), + 'signature': ''.join([chr(x) for x in A3_signature])} + + +# draft-ietf-jose-json-web-signature-41 - A.4 +A4_protected = \ + [123, 34, 97, 108, 103, 34, 58, 34, 69, 83, 53, 49, 50, 34, 125] +A4_payload = [80, 97, 121, 108, 111, 97, 100] +A4_key = \ + {"kty": "EC", + "crv": "P-521", + "x": "AekpBQ8ST8a8VcfVOTNl353vSrDCLLJXmPk06wTjxrrjcBpXp5EOnYG_" + "NjFZ6OvLFV1jSfS9tsz4qUxcWceqwQGk", + "y": "ADSmRA43Z1DSNx_RvcLI87cdL07l6jQyyBXMoxVg_l2Th-x3S1WDhjDl" + "y79ajL4Kkd0AZMaZmh9ubmf63e3kyMj2", + "d": "AY5pb7A0UFiB3RELSD64fTLOSV_jazdF7fLYyuTw8lOfRhWg6Y6rUrPA" + "xerEzgdRhajnu0ferB0d53vM9mE15j2C"} +A4_signature = \ + [1, 220, 12, 129, 231, 171, 194, 209, 232, 135, 233, 117, 247, 105, + 122, 210, 26, 125, 192, 1, 217, 21, 82, 91, 45, 240, 255, 83, 19, + 34, 239, 71, 48, 157, 147, 152, 105, 18, 53, 108, 163, 214, 68, + 231, 62, 153, 150, 106, 194, 164, 246, 72, 143, 138, 24, 50, 129, + 223, 133, 206, 209, 172, 63, 237, 119, 109] + \ + [0, 111, 6, 105, 44, 5, 41, 208, 128, 61, 152, 40, 92, 61, 152, 4, + 150, 66, 60, 69, 247, 196, 170, 81, 193, 199, 78, 59, 194, 169, + 16, 124, 9, 143, 42, 142, 131, 48, 206, 238, 34, 175, 83, 203, + 220, 159, 3, 107, 155, 22, 27, 73, 111, 68, 68, 21, 238, 144, 229, + 232, 148, 188, 222, 59, 242, 103] +A4_example = {'key': A4_key, + 'alg': 'ES512', + 'protected': ''.join([chr(x) for x in A4_protected]), + 'payload': ''.join([chr(x) for x in A4_payload]), + 'signature': ''.join([chr(x) for x in A4_signature])} + + +# draft-ietf-jose-json-web-signature-41 - A.4 +A5_protected = 'eyJhbGciOiJub25lIn0' +A5_payload = A2_payload +A5_key = \ + {"kty": "oct", "k": ""} +A5_signature = "" +A5_example = {'key': A5_key, + 'alg': 'none', + 'protected': base64url_decode(A5_protected), + 'payload': ''.join([chr(x) for x in A5_payload]), + 'signature': A5_signature} + +A6_serialized = \ + '{' + \ + '"payload":' + \ + '"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF' + \ + 'tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",' + \ + '"signatures":[' + \ + '{"protected":"eyJhbGciOiJSUzI1NiJ9",' + \ + '"header":' + \ + '{"kid":"2010-12-29"},' + \ + '"signature":' + \ + '"cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZ' + \ + 'mh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjb' + \ + 'KBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHl' + \ + 'b1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZES' + \ + 'c6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AX' + \ + 'LIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"},' + \ + '{"protected":"eyJhbGciOiJFUzI1NiJ9",' + \ + '"header":' + \ + '{"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"},' + \ + '"signature":' + \ + '"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS' + \ + 'lSApmWQxfKTUJqPP3-Kg6NU1Q"}]' + \ + '}' +A6_example = { + 'payload': ''.join([chr(x) for x in A2_payload]), + 'key1': jwk.JWK(**A2_key), # pylint: disable=star-args + 'protected1': ''.join([chr(x) for x in A2_protected]), + 'header1': json.dumps({"kid": "2010-12-29"}), + 'key2': jwk.JWK(**A3_key), # pylint: disable=star-args + 'protected2': ''.join([chr(x) for x in A3_protected]), + 'header2': json.dumps({"kid": "e9bc097a-ce51-4036-9562-d2ade882db0d"}), + 'serialized': A6_serialized} + +A7_example = \ + '{' + \ + '"payload":' + \ + '"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF' + \ + 'tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",' + \ + '"protected":"eyJhbGciOiJFUzI1NiJ9",' + \ + '"header":' + \ + '{"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"},' + \ + '"signature":' + \ + '"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS' + \ + 'lSApmWQxfKTUJqPP3-Kg6NU1Q"' + \ + '}' + +E_negative = \ + 'eyJhbGciOiJub25lIiwNCiAiY3JpdCI6WyJodHRwOi8vZXhhbXBsZS5jb20vVU5ERU' + \ + 'ZJTkVEIl0sDQogImh0dHA6Ly9leGFtcGxlLmNvbS9VTkRFRklORUQiOnRydWUNCn0.' + \ + 'RkFJTA.' + + +class TestJWS(unittest.TestCase): + def check_sign(self, test): + S = jws.JWSCore(test['alg'], + jwk.JWK(**test['key']), + test['protected'], + test['payload']) + sig = S.sign() + decsig = base64url_decode(sig['signature']) + S.verify(decsig) + # ECDSA signatures are always different every time + # they are generated unlike RSA or symmetric ones + if test['key']['kty'] != 'EC': + self.assertEqual(decsig, test['signature']) + else: + # Check we can verify the test signature independently + # this is so taht we can test the ECDSA agaist a known + # good signature + S.verify(test['signature']) + + def test_A1(self): + self.check_sign(A1_example) + + def test_A2(self): + self.check_sign(A2_example) + + def test_A3(self): + self.check_sign(A3_example) + + def test_A4(self): + self.check_sign(A4_example) + + def test_A5(self): + self.check_sign(A5_example) + + def test_A6(self): + S = jws.JWS(A6_example['payload']) + S.add_signature(A6_example['key1'], None, + A6_example['protected1'], + A6_example['header1']) + S.add_signature(A6_example['key2'], None, + A6_example['protected2'], + A6_example['header2']) + sig = S.serialize() + S.deserialize(sig, A6_example['key1']) + S.deserialize(A6_serialized, A6_example['key2']) + + def test_A7(self): + S = jws.JWS(A6_example['payload']) + S.deserialize(A7_example, A6_example['key2']) + + def test_E(self): + S = jws.JWS(A6_example['payload']) + S.deserialize(E_negative) + self.assertEqual(False, S.objects['valid']) diff --git a/requirements.txt b/requirements.txt index 24ebb3a..778c982 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -cryptography >= 0.6 +cryptography >= 0.7.2 |