From 9a36f12f15552467ccdaa855aa036f73a7305396 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Wed, 4 Mar 2015 21:25:09 -0500 Subject: Add JWK implementation Implements: draft-ietf-jose-json-web-key-41 plus Tests Signed-off-by: Simo Sorce --- jwcrypto/jwk.py | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ jwcrypto/tests.py | 178 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 432 insertions(+) create mode 100644 jwcrypto/jwk.py create mode 100644 jwcrypto/tests.py diff --git a/jwcrypto/jwk.py b/jwcrypto/jwk.py new file mode 100644 index 0000000..25744ef --- /dev/null +++ b/jwcrypto/jwk.py @@ -0,0 +1,254 @@ +# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric import ec +from jwcrypto.common import base64url_decode +import json + +# draft-ietf-jose-json-web-algorithms-24 - 7.4 +JWKTypesRegistry = {'EC': 'Elliptic Curve', + 'RSA': 'RSA', + 'oct': 'Octet sequence'} + +# draft-ietf-jose-json-web-algorithms-24 - 7.5 +# It is part of the JWK Parameters Registry, but we want a more +# specific map for internal usage +JWKValuesRegistry = {'EC': {'crv': ('Curve', 'Public'), + 'x': ('X Coordinate', 'Public'), + 'y': ('Y Coordinate', 'Public'), + 'd': ('ECC Private Key', 'Private')}, + 'RSA': {'n': ('Modulus', 'Public'), + 'e': ('Exponent', 'Public'), + 'd': ('Private Exponent', 'Private'), + 'p': ('First Prime Factor', 'Private'), + 'q': ('Second Prime Factor', 'Private'), + 'dp': ('First Factor CRT Exponent', 'Private'), + 'dq': ('Second Factor CRT Exponent', 'Private'), + 'qi': ('First CRT Coefficient', 'Private')}, + 'oct': {'k': ('Key Value', 'Private')}} + +JWKParamsRegistry = {'kty': ('Key Type', 'Public', ), + 'use': ('Public Key Use', 'Public'), + 'key_ops': ('Key Operations', 'Public'), + 'alg': ('Algorithm', 'Public'), + 'kid': ('Key ID', 'Public'), + 'x5u': ('X.509 URL', 'Public'), + 'x5c': ('X.509 Certificate Chain', 'Public'), + 'x5t': ('X.509 Certificate SHA-1 Thumbprint', 'Public'), + 'x5t#S256': ('X.509 Certificate SHA-256 Thumbprint', + 'Public')} + +# draft-ietf-jose-json-web-algorithms-24 - 7.6 +JWKEllipticCurveRegistry = {'P-256': 'P-256 curve', + 'P-384': 'P-384 curve', + 'P-521': 'P-521 curve'} + +# draft-ietf-jose-json-web-key-41 - 8.2 +JWKUseRegistry = {'sig': 'Digital Signature or MAC', + 'enc': 'Encryption'} + +# draft-ietf-jose-json-web-key-41 - 8.2 +JWKOperationsRegistry = {'sign': 'Compute digital Signature or MAC', + 'verify': 'Verify digital signature or MAC', + 'encrypt': 'Encrypt content', + 'decrypt': 'Decrypt content and validate' + ' decryption, if applicable', + 'wrapKey': 'Encrypt key', + 'unwrapKey': 'Decrypt key and validate' + ' decryption, if applicable', + 'deriveKey': 'Derive key', + 'deriveBits': 'Derive bits not to be used as a key'} + + +class InvalidJWKType(Exception): + + def __init__(self, value=None): + super(InvalidJWKType, self).__init__() + self.value = value + + def __str__(self): + return 'Unknown type "%s", valid types are: %s' % ( + self.value, JWKTypesRegistry.keys()) + + +class InvalidJWKUsage(Exception): + + def __init__(self, use, value): + super(InvalidJWKUsage, self).__init__() + self.value = value + self.use = use + + def __str__(self): + if self.use in JWKUseRegistry.keys(): + usage = JWKUseRegistry[self.use] + else: + usage = 'Unknown(%s)' % self.use + if self.value in JWKUseRegistry.keys(): + valid = JWKUseRegistry[self.value] + else: + valid = 'Unknown(%s)' % self.value + return 'Invalid usage requested: "%s". Valid for: "%s"' % (usage, + valid) + + +class InvalidJWKOperation(Exception): + + def __init__(self, operation, values): + super(InvalidJWKOperation, self).__init__() + self.op = operation + self.values = values + + def __str__(self): + if self.op in JWKOperationsRegistry.keys(): + op = JWKOperationsRegistry[self.op] + else: + op = 'Unknown(%s)' % self.op + valid = list() + for v in self.values: + if v in JWKOperationsRegistry.keys(): + valid.append(JWKOperationsRegistry[v]) + else: + valid.append('Unknown(%s)' % v) + return 'Invalid operation requested: "%s". Valid for: "%s"' % (op, + valid) + + +class InvalidJWKValue(Exception): + pass + + +class JWK(object): + + def __init__(self, **kwargs): + + names = kwargs.keys() + + self._params = dict() + for name in JWKParamsRegistry.keys(): + if name in kwargs: + self._params[name] = kwargs[name] + while name in names: + names.remove(name) + + kty = self._params.get('kty', None) + if kty not in JWKTypesRegistry: + raise InvalidJWKType(kty) + + self._key = dict() + for name in JWKValuesRegistry[kty].keys(): + if name in kwargs: + self._key[name] = kwargs[name] + while name in names: + names.remove(name) + + if len(names) != 0: + raise InvalidJWKValue('Unknown key parameters: %s' % names) + + if len(self._key) == 0: + raise InvalidJWKValue('No Key Values found') + + def export(self): + d = dict() + d.update(self._params) + d.update(self._key) + return json.dumps(d) + + @property + def key_id(self): + return self._params.get('kid', None) + + def get_curve(self, arg): + k = self._key + if self._params['kty'] != 'EC': + raise InvalidJWKType('Not an EC key') + if arg and k['crv'] != arg: + raise InvalidJWKValue('Curve requested is "%s", but ' + 'key curve is "%s"' % (arg, k['crv'])) + if k['crv'] == 'P-256': + return ec.SECP256R1() + elif k['crv'] == 'P-384': + return ec.SECP384R1() + elif k['crv'] == 'P-521': + return ec.SECP521R1() + else: + raise InvalidJWKValue('Unknown Elliptic Curve Type') + + def _check_constraints(self, usage, operation): + use = self._params.get('use', None) + if use and use != usage: + raise InvalidJWKUsage(usage, use) + ops = self._params.get('key_ops', None) + if ops: + if not isinstance(ops, list): + ops = [ops] + if operation not in ops: + raise InvalidJWKOperation(operation, ops) + # TODO: check alg ? + + def _decode_int(self, n): + return int(base64url_decode(n).encode('hex'), 16) + + def sign_key(self, arg=None): + self._check_constraints('sig', 'sign') + if self._params['kty'] == 'oct': + return self._key['k'] + elif self._params['kty'] == 'RSA': + k = self._key + pub = rsa.RSAPublicNumbers(self._decode_int(k['e']), + self._decode_int(k['n'])) + pri = rsa.RSAPrivateNumbers(self._decode_int(k['p']), + self._decode_int(k['q']), + self._decode_int(k['d']), + self._decode_int(k['dp']), + self._decode_int(k['dq']), + self._decode_int(k['qi']), pub) + return pri.private_key(default_backend()) + elif self._params['kty'] == 'EC': + k = self._key + pub = ec.EllipticCurvePublicNumbers(self._decode_int(k['x']), + self._decode_int(k['y']), + self.get_curve(arg)) + pri = ec.EllipticCurvePrivateNumbers(self._decode_int(k['d']), + pub) + return pri.private_key(default_backend()) + else: + raise NotImplementedError + + def verify_key(self, arg=None): + self._check_constraints('sig', 'verify') + if self._params['kty'] == 'oct': + return self._key['k'] + elif self._params['kty'] == 'RSA': + k = self._key + pub = rsa.RSAPublicNumbers(self._decode_int(k['e']), + self._decode_int(k['n'])) + return pub.public_key(default_backend()) + elif self._params['kty'] == 'EC': + k = self._key + pub = ec.EllipticCurvePublicNumbers(self._decode_int(k['x']), + self._decode_int(k['y']), + self.get_curve(arg)) + return pub.public_key(default_backend()) + else: + raise NotImplementedError + + +class JWKSet(set): + + def add(self, elem): + if not isinstance(elem, JWK): + raise TypeError('Only JWK objects are valid elements') + set.add(self, elem) + + def export(self): + keys = list() + for jwk in self: + keys.append(json.loads(jwk.export())) + return json.dumps({'keys': keys}) + + def get_key(self, kid): + for jwk in self: + if jwk.key_id == kid: + return jwk + return None diff --git a/jwcrypto/tests.py b/jwcrypto/tests.py new file mode 100644 index 0000000..8e6c14c --- /dev/null +++ b/jwcrypto/tests.py @@ -0,0 +1,178 @@ +# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file + +from jwcrypto import jwk +import json +import unittest + +# draft-ietf-jose-json-web-key-41 - A.1 +PublicKeys = {"keys": [ + {"kty": "EC", + "crv": "P-256", + "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "use": "enc", + "kid": "1"}, + {"kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbf" + "AAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknj" + "hMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65" + "YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQ" + "vRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lF" + "d2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzK" + "nqDKgw", + "e": "AQAB", + "alg": "RS256", + "kid": "2011-04-29"}]} + +# draft-ietf-jose-json-web-key-41 - A.2 +PrivateKeys = {"keys": [ + {"kty": "EC", + "crv": "P-256", + "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", + "use": "enc", + "kid": "1"}, + {"kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbb" + "fAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3ok" + "njhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v" + "-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu" + "6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0" + "fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8a" + "wapJzKnqDKgw", + "e": "AQAB", + "d": "X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7d" + "x5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_" + "YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywb" + "ReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5Zi" + "G7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z" + "4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc" + "0X4jfcKoAC8Q", + "p": "83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVn" + "wD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XO" + "uVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O" + "0nVbfs", + "q": "3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumq" + "jVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VV" + "S78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb" + "6yelxk", + "dp": "G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimY" + "wxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA" + "77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8Y" + "eiKkTiBj0", + "dq": "s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUv" + "MfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqU" + "fLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txX" + "w494Q_cgk", + "qi": "GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgU" + "IZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_" + "mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia" + "6zTKhAVRU", + "alg": "RS256", + "kid": "2011-04-29"}]} + +# draft-ietf-jose-json-web-key-41 - A.3 +SymmetricKeys = {"keys": [ + {"kty": "oct", + "alg": "A128KW", + "k": "GawgguFyGrWKav7AX4VKUg"}, + {"kty": "oct", + "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH7" + "5aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", + "kid": "HMAC key used in JWS A.1 example"}]} + +# draft-ietf-jose-json-web-key-41 - B +Useofx5c = {"kty": "RSA", + "use": "sig", + "kid": "1b94c", + "n": "vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08PLbK" + "_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Qu2j" + "8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4aYW" + "Achc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwHM" + "TplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMv" + "VfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ", + "e": "AQAB", + "x5c": ["MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJ" + "BgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRww" + "GgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5Ccmlh" + "biBDYW1wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVa" + "MGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVu" + "dmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQD" + "Ew5CcmlhbiBDYW1wYmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC" + "AQoCggEBAL64zn8/QnHYMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9" + "if6amFtPDy2yvz3YlRij66s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u" + "3WG7K+IiZhtELto/A7Fck9Ws6SQvzRvOE8uSirYbgmj6He4iO8NCyvaK" + "0jIQRMMGQwsU1quGmFgHIXPLfnpnfajr1rVTAwtgV5LEZ4Iel+W1GC8u" + "gMhyr4/p1MtcIM42EA8BzE6ZQqC7VPqPvEjZ2dbZkaBhPbiZAS3YeYBR" + "DWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVkaZdklLQp2Btgt9qr21m42f" + "4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAh8zGlfSl" + "cI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL+9gGlqCz5iWLOgWs" + "nrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1zFo+Owb1zxtp3" + "PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL2Bo3UPGrps" + "HzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo4tpzd5r" + "FXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTqgawR" + "+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA==" + ]} + +# draft-ietf-jose-json-web-key-41 - C.1 +RSAPrivateKey = {"kty": "RSA", + "kid": "juliet@capulet.lit", + "use": "enc", + "n": "t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNq" + "FMSQRyO125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR" + "0-Iqom-QFcNP8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQ" + "lO8Yns5jCtLCRwLHL0Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-" + "AqWS9zIQ2ZilgT-GqUmipg0XOC0Cc20rgLe2ymLHjpHciCKVAbY5-L" + "32-lSeZO-Os6U15_aXrk9Gw8cPUaX1_I8sLGuSiVdt3C_Fn2PZ3Z8i" + "744FPFGGcG1qs2Wz-Q", + "e": "AQAB", + "d": "GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTea" + "STyWfSNkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWa" + "Cl3hdlPKXy9UvqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo" + "4_PMaenNnPiQgO0xnuToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDms" + "XOfUENOyMqADC6p1M3h33tsurY15k9qMSpG9OX_IJAXmxzAh_tWiZO" + "wk2K4yxH9tS3Lq1yX8C1EWmeRDkK2ahecG85-oLKQt5VEpWHKmjOi_" + "gJSdSgqcN96X52esAQ", + "p": "2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9u" + "w-PIHfQP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPP" + "SYB9yk31s0Q8UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3r" + "CT5T3yJws", + "q": "1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjs" + "Zu0c6Iedis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjV" + "tG6TlV8CLCYKrYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5" + "B0f808I4s", + "dp": "KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwK" + "qvVDq3tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_l" + "hqigI4y_kqS1wY52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttW" + "txVqLCRViD6c", + "dq": "AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1" + "xDkbN9GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCz" + "kOkmxIe3KRbBymXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRF" + "COJ3xDea-ots", + "qi": "lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEo" + "PwmUqqabu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDM" + "eAvmj4sm-Fp0oYu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu" + "9HCJ-UsfSOI8"} + + +class TestJWK(unittest.TestCase): + def test_create(self): + keylist = PublicKeys['keys'] + for key in keylist: + _ = jwk.JWK(**key) # pylint: disable=star-args + + keylist = PrivateKeys['keys'] + for key in keylist: + _ = jwk.JWK(**key) # pylint: disable=star-args + + keylist = SymmetricKeys['keys'] + for key in keylist: + jwkey = jwk.JWK(**key) # pylint: disable=star-args + _ = jwkey.sign_key() + _ = jwkey.verify_key() + e = jwkey.export() + self.assertEqual(json.loads(e), key) + + _ = jwk.JWK(**Useofx5c) # pylint: disable=star-args + _ = jwk.JWK(**RSAPrivateKey) # pylint: disable=star-args -- cgit