summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimo Sorce <simo@redhat.com>2015-03-04 21:22:05 -0500
committerSimo Sorce <simo@redhat.com>2015-03-08 16:19:37 -0400
commitc48d7b2e49e779f0593e98dddb9f4aa11d5beb6c (patch)
tree00c2070e093de27cbd0d993adb2c06444855330a
parent9a36f12f15552467ccdaa855aa036f73a7305396 (diff)
downloadjwcrypto-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.py8
-rw-r--r--jwcrypto/jws.py461
-rw-r--r--jwcrypto/tests.py260
-rw-r--r--requirements.txt2
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