diff -Nru python-jwcrypto-0.6.0/debian/changelog python-jwcrypto-0.8.0/debian/changelog --- python-jwcrypto-0.6.0/debian/changelog 2019-09-11 21:04:33.000000000 +0000 +++ python-jwcrypto-0.8.0/debian/changelog 2021-01-05 09:49:19.000000000 +0000 @@ -1,3 +1,14 @@ +python-jwcrypto (0.8.0-1) unstable; urgency=medium + + [ Debian Janitor ] + * New upstream release. + * Set debhelper-compat version in Build-Depends. + * Set upstream metadata fields: Bug-Database, Repository, Repository- + Browse. + * Update standards version to 4.5.1, no changes needed. + + -- Timo Aaltonen Tue, 05 Jan 2021 11:49:19 +0200 + python-jwcrypto (0.6.0-2) unstable; urgency=medium * compat, control: Bump compat to 12. diff -Nru python-jwcrypto-0.6.0/debian/compat python-jwcrypto-0.8.0/debian/compat --- python-jwcrypto-0.6.0/debian/compat 2019-09-11 21:01:39.000000000 +0000 +++ python-jwcrypto-0.8.0/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -12 diff -Nru python-jwcrypto-0.6.0/debian/control python-jwcrypto-0.8.0/debian/control --- python-jwcrypto-0.6.0/debian/control 2019-09-11 21:04:17.000000000 +0000 +++ python-jwcrypto-0.8.0/debian/control 2021-01-05 09:45:14.000000000 +0000 @@ -4,13 +4,13 @@ Section: python Priority: optional Build-Depends: - debhelper (>= 12), + debhelper-compat (= 12), dh-python, python3-all, python3-cryptography, python3-nose, python3-setuptools, -Standards-Version: 4.4.0 +Standards-Version: 4.5.1 Homepage: https://github.com/latchset/jwcrypto Vcs-Git: https://salsa.debian.org/freeipa-team/python-jwcrypto.git Vcs-Browser: https://salsa.debian.org/freeipa-team/python-jwcrypto diff -Nru python-jwcrypto-0.6.0/debian/upstream/metadata python-jwcrypto-0.8.0/debian/upstream/metadata --- python-jwcrypto-0.6.0/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ python-jwcrypto-0.8.0/debian/upstream/metadata 2021-01-05 09:44:27.000000000 +0000 @@ -0,0 +1,3 @@ +Bug-Database: https://github.com/latchset/jwcrypto/issues +Repository: https://github.com/latchset/jwcrypto.git +Repository-Browse: https://github.com/latchset/jwcrypto diff -Nru python-jwcrypto-0.6.0/docs/source/conf.py python-jwcrypto-0.8.0/docs/source/conf.py --- python-jwcrypto-0.6.0/docs/source/conf.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/docs/source/conf.py 2020-08-17 19:48:18.000000000 +0000 @@ -53,9 +53,9 @@ # built documents. # # The short X.Y version. -version = '0.6' +version = '0.8' # The full version, including alpha/beta/rc tags. -release = '0.6' +release = '0.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff -Nru python-jwcrypto-0.6.0/docs/source/jwe.rst python-jwcrypto-0.8.0/docs/source/jwe.rst --- python-jwcrypto-0.6.0/docs/source/jwe.rst 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/docs/source/jwe.rst 2020-08-17 19:48:18.000000000 +0000 @@ -3,7 +3,7 @@ The jwe Module implements the `JSON Web Encryption`_ standard. A JSON Web Encryption is represented by a JWE object, related utility -classes and functions are availbale in this module too. +classes and functions are available in this module too. .. _JSON Web Encryption: https://tools.ietf.org/html/rfc7516 diff -Nru python-jwcrypto-0.6.0/docs/source/jwk.rst python-jwcrypto-0.8.0/docs/source/jwk.rst --- python-jwcrypto-0.6.0/docs/source/jwk.rst 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/docs/source/jwk.rst 2020-08-17 19:48:18.000000000 +0000 @@ -3,7 +3,7 @@ The jwk Module implements the `JSON Web Key`_ standard. A JSON Web Key is represented by a JWK object, related utility classes and -functions are availbale in this module too. +functions are available in this module too. .. _JSON Web Key: http://tools.ietf.org/html/rfc7517 @@ -69,10 +69,10 @@ >>> key.export() '{"k":"X6TBlwY2so8EwKZ2TFXM7XHSgWBKQJhcspzYydp5Y-o","kty":"oct"}' -Create a 2048bit RSA keypair:: +Create a 2048bit RSA key pair:: >>> jwk.JWK.generate(kty='RSA', size=2048) -Create a P-256 EC keypair and export the public key:: +Create a P-256 EC key pair and export the public key:: >>> key = jwk.JWK.generate(kty='EC', crv='P-256') >>> key.export(private_key=False) '{"y":"VYlYwBfOTIICojCPfdUjnmkpN-g-lzZKxzjAoFmDRm8", diff -Nru python-jwcrypto-0.6.0/docs/source/jwt.rst python-jwcrypto-0.8.0/docs/source/jwt.rst --- python-jwcrypto-0.6.0/docs/source/jwt.rst 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/docs/source/jwt.rst 2020-08-17 19:48:18.000000000 +0000 @@ -3,7 +3,7 @@ The jwt Module implements the `JSON Web Token`_ standard. A JSON Web Token is represented by a JWT object, related utility classes and -functions are availbale in this module too. +functions are available in this module too. .. _JSON Web Token: http://tools.ietf.org/html/rfc7519 diff -Nru python-jwcrypto-0.6.0/jwcrypto/common.py python-jwcrypto-0.8.0/jwcrypto/common.py --- python-jwcrypto-0.6.0/jwcrypto/common.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/jwcrypto/common.py 2020-08-17 19:48:18.000000000 +0000 @@ -1,8 +1,13 @@ # Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file +import copy import json from base64 import urlsafe_b64decode, urlsafe_b64encode - +from collections import namedtuple +try: + from collections.abc import MutableMapping +except ImportError: + from collections import MutableMapping # Padding stripping versions as described in # RFC 7515 Appendix C @@ -56,7 +61,7 @@ """Invalid CEK Key Length. This exception is raised when a Content Encryption Key does not match - the required lenght. + the required length. """ def __init__(self, expected, obtained): @@ -86,7 +91,7 @@ """Invalid JWE Key Type. This exception is raised when the provided JWK Key does not match - the type required by the sepcified algorithm. + the type required by the specified algorithm. """ def __init__(self, expected, obtained): @@ -98,9 +103,91 @@ """Invalid JWE Key Length. This exception is raised when the provided JWK Key does not match - the lenght required by the sepcified algorithm. + the length required by the specified algorithm. """ def __init__(self, expected, obtained): - msg = 'Expected key of lenght %d, got %d' % (expected, obtained) + msg = 'Expected key of length %d, got %d' % (expected, obtained) super(InvalidJWEKeyLength, self).__init__(msg) + + +class InvalidJWSERegOperation(JWException): + """Invalid JWSE Header Registry Operation. + + This exception is raised when there is an error in trying ot add a JW + Signature or Encryption header to the Registry. + """ + + def __init__(self, message=None, exception=None): + msg = None + if message: + msg = message + else: + msg = 'Unknown Operation Failure' + if exception: + msg += ' {%s}' % repr(exception) + super(InvalidJWSERegOperation, self).__init__(msg) + + +# JWSE Header Registry definitions + +# RFC 7515 - 9.1: JSON Web Signature and Encryption Header Parameters Registry +# HeaderParameters are for both JWS and JWE +JWSEHeaderParameter = namedtuple('Parameter', + 'description mustprotect supported check_fn') + + +class JWSEHeaderRegistry(MutableMapping): + def __init__(self, init_registry=None): + if init_registry: + if isinstance(init_registry, dict): + self._registry = copy.deepcopy(init_registry) + else: + raise InvalidJWSERegOperation('Unknown input type') + else: + self._registry = {} + + MutableMapping.__init__(self) + + def check_header(self, h, value): + if h not in self._registry: + raise InvalidJWSERegOperation('No header "%s" found in registry' + % h) + + param = self._registry[h] + if param.check_fn is None: + return True + else: + return param.check_fn(value) + + def __getitem__(self, key): + return self._registry.__getitem__(key) + + def __iter__(self): + return self._registry.__iter__() + + def __delitem__(self, key): + if self._registry[key].mustprotect or \ + self._registry[key].supported: + raise InvalidJWSERegOperation('Unable to delete protected or ' + 'supported field') + else: + self._registry.__delitem__(key) + + def __setitem__(self, h, jwse_header_param): + # Check if a header is not supported + if h in self._registry: + p = self._registry[h] + if p.supported: + raise InvalidJWSERegOperation('Supported header already exists' + ' in registry') + elif p.mustprotect and not jwse_header_param.mustprotect: + raise InvalidJWSERegOperation('Header specified should be' + 'a protected header') + else: + del self._registry[h] + + self._registry[h] = jwse_header_param + + def __len__(self): + return self._registry.__len__() diff -Nru python-jwcrypto-0.6.0/jwcrypto/jwa.py python-jwcrypto-0.8.0/jwcrypto/jwa.py --- python-jwcrypto-0.6.0/jwcrypto/jwa.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/jwcrypto/jwa.py 2020-08-17 19:48:18.000000000 +0000 @@ -70,7 +70,7 @@ def _randombits(x): if x % 8 != 0: - raise ValueError("lenght must be a multiple of 8") + raise ValueError("length must be a multiple of 8") return os.urandom(_inbytes(x)) @@ -161,7 +161,8 @@ return '' def verify(self, key, payload, signature): - raise InvalidSignature('The "none" signature cannot be verified') + if key.key_type != 'oct' or key.get_op_key() != '': + raise InvalidSignature('The "none" signature cannot be verified') class _HS256(_RawHMAC, JWAAlgorithm): @@ -248,6 +249,18 @@ super(_ES256, self).__init__('P-256', hashes.SHA256()) +class _ES256K(_RawEC, JWAAlgorithm): + + name = "ES256K" + description = "ECDSA using secp256k1 curve and SHA-256" + keysize = 256 + algorithm_usage_location = 'alg' + algorithm_use = 'sig' + + def __init__(self): + super(_ES256K, self).__init__('secp256k1', hashes.SHA256()) + + class _ES384(_RawEC, JWAAlgorithm): name = "ES384" @@ -693,8 +706,12 @@ def _check_key(self, key): if not isinstance(key, JWK): raise ValueError('key is not a JWK object') - if key.key_type != 'EC': - raise InvalidJWEKeyType('EC', key.key_type) + if key.key_type not in ['EC', 'OKP']: + raise InvalidJWEKeyType('EC or OKP', key.key_type) + if key.key_type == 'OKP': + if key.key_curve not in ['X25519', 'X448']: + raise InvalidJWEKeyType('X25519 or X448', + key.key_curve) def _derive(self, privkey, pubkey, alg, bitsize, headers): # OtherInfo is defined in NIST SP 56A 5.8.1.2.1 @@ -718,7 +735,13 @@ # no SuppPrivInfo - shared_key = privkey.exchange(ec.ECDH(), pubkey) + # Shared Key generation + if isinstance(privkey, ec.EllipticCurvePrivateKey): + shared_key = privkey.exchange(ec.ECDH(), pubkey) + else: + # X25519/X448 + shared_key = privkey.exchange(pubkey) + ckdf = ConcatKDFHash(algorithm=hashes.SHA256(), length=_inbytes(bitsize), otherinfo=otherinfo, @@ -802,6 +825,28 @@ algorithm_use = 'kex' +class _EdDsa(_RawJWS, JWAAlgorithm): + + name = 'EdDSA' + description = 'EdDSA using Ed25519 or Ed448 algorithms' + algorithm_usage_location = 'alg' + algorithm_use = 'sig' + keysize = None + + def sign(self, key, payload): + + if key.key_curve in ['Ed25519', 'Ed448']: + skey = key.get_op_key('sign') + return skey.sign(payload) + raise NotImplementedError + + def verify(self, key, payload, signature): + if key.key_curve in ['Ed25519', 'Ed448']: + pkey = key.get_op_key('verify') + return pkey.verify(signature, payload) + raise NotImplementedError + + class _RawJWE(object): def encrypt(self, k, a, m): @@ -1009,6 +1054,7 @@ 'RS384': _RS384, 'RS512': _RS512, 'ES256': _ES256, + 'ES256K': _ES256K, 'ES384': _ES384, 'ES512': _ES512, 'PS256': _PS256, @@ -1026,6 +1072,7 @@ 'ECDH-ES+A128KW': _EcdhEsAes128Kw, 'ECDH-ES+A192KW': _EcdhEsAes192Kw, 'ECDH-ES+A256KW': _EcdhEsAes256Kw, + 'EdDSA': _EdDsa, 'A128GCMKW': _A128GcmKw, 'A192GCMKW': _A192GcmKw, 'A256GCMKW': _A256GcmKw, diff -Nru python-jwcrypto-0.6.0/jwcrypto/jwe.py python-jwcrypto-0.8.0/jwcrypto/jwe.py --- python-jwcrypto-0.6.0/jwcrypto/jwe.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/jwcrypto/jwe.py 2020-08-17 19:48:18.000000000 +0000 @@ -4,6 +4,7 @@ from jwcrypto import common from jwcrypto.common import JWException +from jwcrypto.common import JWSEHeaderParameter, JWSEHeaderRegistry from jwcrypto.common import base64url_decode, base64url_encode from jwcrypto.common import json_decode, json_encode from jwcrypto.jwa import JWA @@ -11,20 +12,23 @@ # RFC 7516 - 4.1 # name: (description, supported?) -JWEHeaderRegistry = {'alg': ('Algorithm', True), - 'enc': ('Encryption Algorithm', True), - 'zip': ('Compression 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)} +JWEHeaderRegistry = { + 'alg': JWSEHeaderParameter('Algorithm', False, True, None), + 'enc': JWSEHeaderParameter('Encryption Algorithm', False, True, None), + 'zip': JWSEHeaderParameter('Compression Algorithm', False, True, None), + 'jku': JWSEHeaderParameter('JWK Set URL', False, False, None), + 'jwk': JWSEHeaderParameter('JSON Web Key', False, False, None), + 'kid': JWSEHeaderParameter('Key ID', False, True, None), + 'x5u': JWSEHeaderParameter('X.509 URL', False, False, None), + 'x5c': JWSEHeaderParameter('X.509 Certificate Chain', False, False, None), + 'x5t': JWSEHeaderParameter('X.509 Certificate SHA-1 Thumbprint', False, + False, None), + 'x5t#S256': JWSEHeaderParameter('X.509 Certificate SHA-256 Thumbprint', + False, False, None), + 'typ': JWSEHeaderParameter('Type', False, True, None), + 'cty': JWSEHeaderParameter('Content Type', False, True, None), + 'crit': JWSEHeaderParameter('Critical', True, True, None), +} """Registry of valid header parameters""" default_allowed_algs = [ @@ -73,7 +77,8 @@ """ def __init__(self, plaintext=None, protected=None, unprotected=None, - aad=None, algs=None, recipient=None, header=None): + aad=None, algs=None, recipient=None, header=None, + header_registry=None): """Creates a JWE token. :param plaintext(bytes): An arbitrary plaintext to be encrypted. @@ -83,10 +88,14 @@ :param algs: An optional list of allowed algorithms :param recipient: An optional, default recipient key :param header: An optional header for the default recipient + :param header_registry: Optional additions to the header registry """ self._allowed_algs = None self.objects = dict() self.plaintext = None + self.header_registry = JWSEHeaderRegistry(JWEHeaderRegistry) + if header_registry: + self.header_registry.update(header_registry) if plaintext is not None: if isinstance(plaintext, bytes): self.plaintext = plaintext @@ -275,13 +284,13 @@ "is set" % invalid) if 'protected' not in self.objects: raise InvalidJWEOperation( - "Can't use compat encoding without protected headers") + "Can't use compact encoding without protected headers") else: ph = json_decode(self.objects['protected']) for required in 'alg', 'enc': if required not in ph: raise InvalidJWEOperation( - "Can't use compat encoding, '%s' must be in the " + "Can't use compact encoding, '%s' must be in the " "protected header" % required) if 'recipients' in self.objects: if len(self.objects['recipients']) != 1: @@ -339,10 +348,10 @@ def _check_crit(self, crit): for k in crit: - if k not in JWEHeaderRegistry: + if k not in self.header_registry: raise InvalidJWEData('Unknown critical header: "%s"' % k) else: - if not JWEHeaderRegistry[k][1]: + if not self.header_registry[k].supported: raise InvalidJWEData('Unsupported critical header: ' '"%s"' % k) @@ -354,6 +363,11 @@ # TODO: allow caller to specify list of headers it understands self._check_crit(jh.get('crit', dict())) + for hdr in jh: + if hdr in self.header_registry: + if not self.header_registry.check_header(hdr, self): + raise InvalidJWEData('Failed header check') + alg = self._jwa_keymgmt(jh.get('alg', None)) enc = self._jwa_enc(jh.get('enc', None)) @@ -424,7 +438,7 @@ If a key is provided a decryption step will be attempted after the object is successfully deserialized. - :raises InvalidJWEData: if the raw object is an invaid JWE token. + :raises InvalidJWEData: if the raw object is an invalid JWE token. :raises InvalidJWEOperation: if the decryption fails. """ @@ -492,7 +506,7 @@ @property def jose_header(self): - jh = self._get_jose_header() + jh = self._get_jose_header(self.objects.get('header')) if len(jh) == 0: raise InvalidJWEOperation("JOSE Header not available") return jh diff -Nru python-jwcrypto-0.6.0/jwcrypto/jwk.py python-jwcrypto-0.8.0/jwcrypto/jwk.py --- python-jwcrypto-0.6.0/jwcrypto/jwk.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/jwcrypto/jwk.py 2020-08-17 19:48:18.000000000 +0000 @@ -18,10 +18,76 @@ from jwcrypto.common import json_decode, json_encode -# RFC 7518 - 7.4 +class UnimplementedOKPCurveKey(object): + @classmethod + def generate(cls): + raise NotImplementedError + + @classmethod + def from_public_bytes(cls, *args): + raise NotImplementedError + + @classmethod + def from_private_bytes(cls, *args): + raise NotImplementedError + + +ImplementedOkpCurves = [] + + +# Handle the best we can older versions of python cryptography that +# do not yet implement these interfaces properly +try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PublicKey, Ed25519PrivateKey + ) + ImplementedOkpCurves.append('Ed25519') +except ImportError: + Ed25519PublicKey = UnimplementedOKPCurveKey + Ed25519PrivateKey = UnimplementedOKPCurveKey +try: + from cryptography.hazmat.primitives.asymmetric.ed448 import ( + Ed448PublicKey, Ed448PrivateKey + ) + ImplementedOkpCurves.append('Ed448') +except ImportError: + Ed448PublicKey = UnimplementedOKPCurveKey + Ed448PrivateKey = UnimplementedOKPCurveKey +try: + from cryptography.hazmat.primitives.asymmetric.x25519 import ( + X25519PublicKey, X25519PrivateKey + ) + priv_bytes = getattr(X25519PrivateKey, 'from_private_bytes', None) + if priv_bytes is None: + raise ImportError + ImplementedOkpCurves.append('X25519') +except ImportError: + X25519PublicKey = UnimplementedOKPCurveKey + X25519PrivateKey = UnimplementedOKPCurveKey +try: + from cryptography.hazmat.primitives.asymmetric.x448 import ( + X448PublicKey, X448PrivateKey + ) + ImplementedOkpCurves.append('X448') +except ImportError: + X448PublicKey = UnimplementedOKPCurveKey + X448PrivateKey = UnimplementedOKPCurveKey + + +_OKP_CURVE = namedtuple('Name', 'pubkey privkey') +_OKP_CURVES_TABLE = { + 'Ed25519': _OKP_CURVE(Ed25519PublicKey, Ed25519PrivateKey), + 'Ed448': _OKP_CURVE(Ed448PublicKey, Ed448PrivateKey), + 'X25519': _OKP_CURVE(X25519PublicKey, X25519PrivateKey), + 'X448': _OKP_CURVE(X448PublicKey, X448PrivateKey) +} + + +# RFC 7518 - 7.4 , RFC 8037 - 5 JWKTypesRegistry = {'EC': 'Elliptic Curve', 'RSA': 'RSA', - 'oct': 'Octet sequence'} + 'oct': 'Octet sequence', + 'OKP': 'Octet Key Pair'} """Registry of valid Key Types""" @@ -31,7 +97,7 @@ class ParmType(Enum): name = 'A string with a name' b64 = 'Base64url Encoded' - b64U = 'Base64urlUint Encoded' + b64u = 'Base64urlUint Encoded' unsupported = 'Unsupported Parameter' @@ -45,21 +111,26 @@ }, 'RSA': { 'n': JWKParameter('Modulus', True, True, ParmType.b64), - 'e': JWKParameter('Exponent', True, True, ParmType.b64U), - 'd': JWKParameter('Private Exponent', False, False, ParmType.b64U), - 'p': JWKParameter('First Prime Factor', False, False, ParmType.b64U), - 'q': JWKParameter('Second Prime Factor', False, False, ParmType.b64U), + 'e': JWKParameter('Exponent', True, True, ParmType.b64u), + 'd': JWKParameter('Private Exponent', False, False, ParmType.b64u), + 'p': JWKParameter('First Prime Factor', False, False, ParmType.b64u), + 'q': JWKParameter('Second Prime Factor', False, False, ParmType.b64u), 'dp': JWKParameter('First Factor CRT Exponent', - False, False, ParmType.b64U), + False, False, ParmType.b64u), 'dq': JWKParameter('Second Factor CRT Exponent', - False, False, ParmType.b64U), + False, False, ParmType.b64u), 'qi': JWKParameter('First CRT Coefficient', - False, False, ParmType.b64U), + False, False, ParmType.b64u), 'oth': JWKParameter('Other Primes Info', False, False, ParmType.unsupported), }, 'oct': { 'k': JWKParameter('Key Value', False, True, ParmType.b64), + }, + 'OKP': { + 'crv': JWKParameter('Curve', True, True, ParmType.name), + 'x': JWKParameter('Public Key', True, True, ParmType.b64), + 'd': JWKParameter('Private Key', False, False, ParmType.b64), } } """Registry of valid key values""" @@ -77,12 +148,18 @@ 'x5t#S256': JWKParameter('X.509 Certificate SHA-256 Thumbprint', True, None, None) } -"""Regstry of valid key parameters""" +"""Registry of valid key parameters""" -# RFC 7518 - 7.6 +# RFC 7518 - 7.6 , RFC 8037 - 5 +# secp256k1 - https://tools.ietf.org/html/draft-ietf-cose-webauthn-algorithms JWKEllipticCurveRegistry = {'P-256': 'P-256 curve', 'P-384': 'P-384 curve', - 'P-521': 'P-521 curve'} + 'P-521': 'P-521 curve', + 'secp256k1': 'SECG secp256k1 curve', + 'Ed25519': 'Ed25519 signature algorithm key pairs', + 'Ed448': 'Ed448 signature algorithm key pairs', + 'X25519': 'X25519 function key pairs', + 'X448': 'X448 function key pairs'} """Registry of allowed Elliptic Curves""" # RFC 7517 - 8.2 @@ -105,7 +182,8 @@ JWKpycaCurveMap = {'secp256r1': 'P-256', 'secp384r1': 'P-384', - 'secp521r1': 'P-521'} + 'secp521r1': 'P-521', + 'secp256k1': 'secp256k1'} class InvalidJWKType(JWException): @@ -203,7 +281,7 @@ always be provided and its value must be a valid one as defined by the 'IANA JSON Web Key Types registry' and specified in the :data:`JWKTypesRegistry` variable. The valid key parameters per - key type are defined in the :data:`JWKValuesregistry` variable. + key type are defined in the :data:`JWKValuesRegistry` variable. To generate a new random key call the class method generate() with the appropriate 'kty' parameter, and other parameters as needed (key @@ -212,12 +290,13 @@ Valid options per type, when generating new keys: * oct: size(int) * RSA: public_exponent(int), size(int) - * EC: curve(str) (one of P-256, P-384, P-521) + * EC: crv(str) (one of P-256, P-384, P-521, secp256k1) + * OKP: crv(str) (one of Ed25519, Ed448, X25519, X448) Deprecated: Alternatively if the 'generate' parameter is provided, with a valid key type as value then a new key will be generated according - to the defaults or provided key strenght options (type specific). + to the defaults or provided key strength options (type specific). :raises InvalidJWKType: if the key type is invalid :raises InvalidJWKValue: if incorrect or inconsistent parameters @@ -274,9 +353,17 @@ params['k'] = base64url_encode(key) self.import_key(**params) - def _encode_int(self, i): - intg = hex(i).rstrip("L").lstrip("0x") - return base64url_encode(unhexlify((len(intg) % 2) * '0' + intg)) + def _encode_int(self, i, bit_size=None): + extend = 0 + if bit_size is not None: + extend = ((bit_size + 7) // 8) * 2 + hexi = hex(i).rstrip("L").lstrip("0x") + hexl = len(hexi) + if extend > hexl: + extend -= hexl + else: + extend = hexl % 2 + return base64url_encode(unhexlify(extend * '0' + hexi)) def _generate_RSA(self, params): pubexp = 65537 @@ -317,6 +404,10 @@ return ec.SECP384R1() elif name == 'P-521': return ec.SECP521R1() + elif name == 'secp256k1': + return ec.SECP256K1() + elif name in _OKP_CURVES_TABLE: + return name else: raise InvalidJWKValue('Unknown Elliptic Curve Type') @@ -334,12 +425,13 @@ def _import_pyca_pri_ec(self, key, **params): pn = key.private_numbers() + key_size = pn.public_numbers.curve.key_size params.update( kty='EC', crv=JWKpycaCurveMap[key.curve.name], - x=self._encode_int(pn.public_numbers.x), - y=self._encode_int(pn.public_numbers.y), - d=self._encode_int(pn.private_value) + x=self._encode_int(pn.public_numbers.x, key_size), + y=self._encode_int(pn.public_numbers.y, key_size), + d=self._encode_int(pn.private_value, key_size) ) self.import_key(**params) @@ -353,6 +445,46 @@ ) self.import_key(**params) + def _generate_OKP(self, params): + if 'crv' not in params: + raise InvalidJWKValue('Must specify "crv" for OKP key generation') + try: + key = _OKP_CURVES_TABLE[params['crv']].privkey.generate() + except KeyError: + raise InvalidJWKValue('"%s" is not a supported curve for the ' + 'OKP key type' % params['crv']) + self._import_pyca_pri_okp(key, **params) + + def _okp_curve_from_pyca_key(self, key): + for name, val in iteritems(_OKP_CURVES_TABLE): + if isinstance(key, (val.pubkey, val.privkey)): + return name + raise InvalidJWKValue('Invalid OKP Key object %r' % key) + + def _import_pyca_pri_okp(self, key, **params): + params.update( + kty='OKP', + crv=self._okp_curve_from_pyca_key(key), + d=base64url_encode(key.private_bytes( + serialization.Encoding.Raw, + serialization.PrivateFormat.Raw, + serialization.NoEncryption())), + x=base64url_encode(key.public_key().public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw)) + ) + self.import_key(**params) + + def _import_pyca_pub_okp(self, key, **params): + params.update( + kty='OKP', + crv=self._okp_curve_from_pyca_key(key), + x=base64url_encode(key.public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw)) + ) + self.import_key(**params) + def import_key(self, **kwargs): names = list(kwargs.keys()) @@ -385,7 +517,7 @@ raise InvalidJWKValue( '"%s" is not base64url encoded' % name ) - if val[3] == ParmType.b64U and name in self._key: + if val[3] == ParmType.b64u and name in self._key: # Check that the value is Base64urlUInt encoded try: self._decode_int(self._key[name]) @@ -444,10 +576,10 @@ obj.import_key(**jkey) return obj - def export(self, private_key=True): + def export(self, private_key=True, as_dict=False): """Exports the key in the standard JSON format. Exports the key regardless of type, if private_key is False - and the key is_symmetric an exceptionis raised. + and the key is_symmetric an exception is raised. :param private_key(bool): Whether to export the private key. Defaults to True. @@ -455,16 +587,20 @@ if private_key is True: # Use _export_all for backwards compatibility, as this # function allows to export symmetrict keys too - return self._export_all() - else: - return self.export_public() + return self._export_all(as_dict) + + return self.export_public(as_dict) - def export_public(self): + def export_public(self, as_dict=False): """Exports the public key in the standard JSON format. It fails if one is not available like when this function is called on a symmetric key. + + :param as_dict(bool): If set to True export as python dict not JSON """ pub = self._public_params() + if as_dict is True: + return pub return json_encode(pub) def _public_params(self): @@ -482,24 +618,28 @@ pub[param] = self._key[param] return pub - def _export_all(self): + def _export_all(self, as_dict=False): d = dict() d.update(self._params) d.update(self._key) d.update(self._unknown) + if as_dict is True: + return d return json_encode(d) - def export_private(self): + def export_private(self, as_dict=False): """Export the private key in the standard JSON format. It fails for a JWK that has only a public key or is symmetric. + + :param as_dict(bool): If set to True export as python dict not JSON """ if self.has_private: - return self._export_all() + return self._export_all(as_dict) raise InvalidJWKType("No private key available") - def export_symmetric(self): + def export_symmetric(self, as_dict=False): if self.is_symmetric: - return self._export_all() + return self._export_all(as_dict) raise InvalidJWKType("Not a symmetric key") def public(self): @@ -547,8 +687,8 @@ @property def key_curve(self): """The Curve Name.""" - if self._params['kty'] != 'EC': - raise InvalidJWKType('Not an EC key') + if self._params['kty'] not in ['EC', 'OKP']: + raise InvalidJWKType('Not an EC or OKP key') return self._key['crv'] def get_curve(self, arg): @@ -556,12 +696,12 @@ :param arg: an optional curve name - :raises InvalidJWKType: the key is not an EC key. + :raises InvalidJWKType: the key is not an EC or OKP key. :raises InvalidJWKValue: if the curve names is invalid. """ k = self._key - if self._params['kty'] != 'EC': - raise InvalidJWKType('Not an EC key') + if self._params['kty'] not in ['EC', 'OKP']: + raise InvalidJWKType('Not an EC or OKP key') if arg and k['crv'] != arg: raise InvalidJWKValue('Curve requested is "%s", but ' 'key curve is "%s"' % (arg, k['crv'])) @@ -605,6 +745,22 @@ return ec.EllipticCurvePrivateNumbers(self._decode_int(k['d']), self._ec_pub(k, curve)) + def _okp_pub(self, k): + try: + pubkey = _OKP_CURVES_TABLE[k['crv']].pubkey + except KeyError: + raise InvalidJWKValue('Unknown curve "%s"' % k['crv']) + + return pubkey.from_public_bytes(base64url_decode(k['x'])) + + def _okp_pri(self, k): + try: + privkey = _OKP_CURVES_TABLE[k['crv']].privkey + except KeyError: + raise InvalidJWKValue('Unknown curve "%s"' % k['crv']) + + return privkey.from_private_bytes(base64url_decode(k['d'])) + def _get_public_key(self, arg=None): if self._params['kty'] == 'oct': return self._key['k'] @@ -612,6 +768,8 @@ return self._rsa_pub(self._key).public_key(default_backend()) elif self._params['kty'] == 'EC': return self._ec_pub(self._key, arg).public_key(default_backend()) + elif self._params['kty'] == 'OKP': + return self._okp_pub(self._key) else: raise NotImplementedError @@ -622,16 +780,18 @@ return self._rsa_pri(self._key).private_key(default_backend()) elif self._params['kty'] == 'EC': return self._ec_pri(self._key, arg).private_key(default_backend()) + elif self._params['kty'] == 'OKP': + return self._okp_pri(self._key) else: raise NotImplementedError def get_op_key(self, operation=None, arg=None): - """Get the key object associated to the requested opration. + """Get the key object associated to the requested operation. For example the public RSA key for the 'verify' operation or the private EC key for the 'decrypt' operation. :param operation: The requested operation. - The valid set of operations is availble in the + The valid set of operations is available in the :data:`JWKOperationsRegistry` registry. :param arg: an optional, context specific, argument For example a curve name. @@ -673,6 +833,10 @@ self._import_pyca_pri_ec(key) elif isinstance(key, ec.EllipticCurvePublicKey): self._import_pyca_pub_ec(key) + elif isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): + self._import_pyca_pri_okp(key) + elif isinstance(key, (Ed25519PublicKey, Ed448PublicKey)): + self._import_pyca_pub_okp(key) else: raise InvalidJWKValue('Unknown key object %r' % key) @@ -791,7 +955,7 @@ class JWKSet(dict): """A set of JWK objects. - Inherits from the standard 'dict' bultin type. + Inherits from the standard 'dict' builtin type. Creates a special key 'keys' that is of a type derived from 'set' The 'keys' attribute accepts only :class:`jwcrypto.jwk.JWK` elements. """ @@ -819,26 +983,31 @@ def add(self, elem): self['keys'].add(elem) - def export(self, private_keys=True): - """Exports a RFC 7517 keyset using the standard JSON format + def export(self, private_keys=True, as_dict=False): + """Exports a RFC 7517 key set. + Exports as json by default, or as dict if requested. :param private_key(bool): Whether to export private keys. Defaults to True. + :param as_dict(bool): Whether to return a dict instead of + a JSON object """ exp_dict = dict() for k, v in iteritems(self): if k == 'keys': keys = list() for jwk in v: - keys.append(json_decode(jwk.export(private_keys))) + keys.append(jwk.export(private_keys, as_dict=True)) v = keys exp_dict[k] = v + if as_dict is True: + return exp_dict return json_encode(exp_dict) def import_keyset(self, keyset): - """Imports a RFC 7517 keyset using the standard JSON format. + """Imports a RFC 7517 key set using the standard JSON format. - :param keyset: The RFC 7517 representation of a JOSE Keyset. + :param keyset: The RFC 7517 representation of a JOSE key set. """ try: jwkset = json_decode(keyset) @@ -857,9 +1026,9 @@ @classmethod def from_json(cls, keyset): - """Creates a RFC 7517 keyset from the standard JSON format. + """Creates a RFC 7517 key set from the standard JSON format. - :param keyset: The RFC 7517 representation of a JOSE Keyset. + :param keyset: The RFC 7517 representation of a JOSE key set. """ obj = cls() obj.import_keyset(keyset) diff -Nru python-jwcrypto-0.6.0/jwcrypto/jws.py python-jwcrypto-0.8.0/jwcrypto/jws.py --- python-jwcrypto-0.6.0/jwcrypto/jws.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/jwcrypto/jws.py 2020-08-17 19:48:18.000000000 +0000 @@ -1,33 +1,27 @@ # Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file -from collections import namedtuple - from jwcrypto.common import JWException +from jwcrypto.common import JWSEHeaderParameter, JWSEHeaderRegistry from jwcrypto.common import base64url_decode, base64url_encode from jwcrypto.common import json_decode, json_encode from jwcrypto.jwa import JWA from jwcrypto.jwk import JWK - -# RFC 7515 - 9.1 -# name: (description, supported?) -JWSHeaderParameter = namedtuple('Parameter', - 'description mustprotect supported') JWSHeaderRegistry = { - 'alg': JWSHeaderParameter('Algorithm', False, True), - 'jku': JWSHeaderParameter('JWK Set URL', False, False), - 'jwk': JWSHeaderParameter('JSON Web Key', False, False), - 'kid': JWSHeaderParameter('Key ID', False, True), - 'x5u': JWSHeaderParameter('X.509 URL', False, False), - 'x5c': JWSHeaderParameter('X.509 Certificate Chain', False, False), - 'x5t': JWSHeaderParameter( - 'X.509 Certificate SHA-1 Thumbprint', False, False), - 'x5t#S256': JWSHeaderParameter( - 'X.509 Certificate SHA-256 Thumbprint', False, False), - 'typ': JWSHeaderParameter('Type', False, True), - 'cty': JWSHeaderParameter('Content Type', False, True), - 'crit': JWSHeaderParameter('Critical', True, True), - 'b64': JWSHeaderParameter('Base64url-Encode Payload', True, True) + 'alg': JWSEHeaderParameter('Algorithm', False, True, None), + 'jku': JWSEHeaderParameter('JWK Set URL', False, False, None), + 'jwk': JWSEHeaderParameter('JSON Web Key', False, False, None), + 'kid': JWSEHeaderParameter('Key ID', False, True, None), + 'x5u': JWSEHeaderParameter('X.509 URL', False, False, None), + 'x5c': JWSEHeaderParameter('X.509 Certificate Chain', False, False, None), + 'x5t': JWSEHeaderParameter( + 'X.509 Certificate SHA-1 Thumbprint', False, False, None), + 'x5t#S256': JWSEHeaderParameter( + 'X.509 Certificate SHA-256 Thumbprint', False, False, None), + 'typ': JWSEHeaderParameter('Type', False, True, None), + 'cty': JWSEHeaderParameter('Content Type', False, True, None), + 'crit': JWSEHeaderParameter('Critical', True, True, None), + 'b64': JWSEHeaderParameter('Base64url-Encode Payload', True, True, None) } """Registry of valid header parameters""" @@ -35,7 +29,8 @@ 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', - 'PS256', 'PS384', 'PS512'] + 'PS256', 'PS384', 'PS512', + 'EdDSA'] """Default allowed algorithms""" @@ -178,16 +173,20 @@ This object represent a JWS token. """ - def __init__(self, payload=None): + def __init__(self, payload=None, header_registry=None): """Creates a JWS object. :param payload(bytes): An arbitrary value (optional). + :param header_registry: Optional additions to the header registry """ self.objects = dict() if payload: self.objects['payload'] = payload self.verifylog = None self._allowed_algs = None + self.header_registry = JWSEHeaderRegistry(JWSHeaderRegistry) + if header_registry: + self.header_registry.update(header_registry) @property def allowed_algs(self): @@ -213,6 +212,7 @@ return self.objects.get('valid', False) # TODO: allow caller to specify list of headers it understands + # FIXME: Merge and check to be changed to two separate functions def _merge_check_headers(self, protected, *headers): header = None crit = [] @@ -221,11 +221,11 @@ crit = protected['crit'] # Check immediately if we support these critical headers for k in crit: - if k not in JWSHeaderRegistry: + if k not in self.header_registry: raise InvalidJWSObject( 'Unknown critical header: "%s"' % k) else: - if not JWSHeaderRegistry[k][1]: + if not self.header_registry[k].supported: raise InvalidJWSObject( 'Unsupported critical header: "%s"' % k) header = protected @@ -239,8 +239,8 @@ if header is None: header = dict() for h in list(hn.keys()): - if h in JWSHeaderRegistry: - if JWSHeaderRegistry[h].mustprotect: + if h in self.header_registry: + if self.header_registry[h].mustprotect: raise InvalidJWSObject('"%s" must be protected' % h) if h in header: raise InvalidJWSObject('Duplicate header: "%s"' % h) @@ -266,7 +266,12 @@ raise InvalidJWSSignature('Invalid Unprotected header') # Merge and check (critical) headers - self._merge_check_headers(p, header) + chk_hdrs = self._merge_check_headers(p, header) + for hdr in chk_hdrs: + if hdr in self.header_registry: + if not self.header_registry.check_header(hdr, self): + raise InvalidJWSSignature('Failed header check') + # check 'alg' is present if alg is None and 'alg' not in p: raise InvalidJWSSignature('No "alg" in headers') @@ -321,7 +326,7 @@ except Exception as e: # pylint: disable=broad-except self.verifylog.append('Failed: [%s]' % repr(e)) else: - raise InvalidJWSSignature('No signatures availble') + raise InvalidJWSSignature('No signatures available') if not self.is_valid: raise InvalidJWSSignature('Verification failed for all ' @@ -368,7 +373,7 @@ :param alg: The signing algorithm (optional). usually the algorithm is known as it is provided with the JOSE Headers of the token. - :raises InvalidJWSObject: if the raw object is an invaid JWS token. + :raises InvalidJWSObject: if the raw object is an invalid JWS token. :raises InvalidJWSSignature: if the verification fails. """ self.objects = dict() @@ -419,7 +424,7 @@ :param alg: An optional algorithm name. If already provided as an element of the protected or unprotected header it can be safely omitted. - :param potected: The Protected Header (optional) + :param protected: The Protected Header (optional) :param header: The Unprotected Header (optional) :raises InvalidJWSObject: if no payload has been set on the object, @@ -475,7 +480,9 @@ if alg is None: raise ValueError('"alg" not specified') - c = JWSCore(alg, key, protected, self.objects['payload']) + c = JWSCore( + alg, key, protected, self.objects['payload'], self.allowed_algs + ) sig = c.sign() o = dict() @@ -511,7 +518,7 @@ representation, otherwise generates a standard JSON format. :raises InvalidJWSOperation: if the object cannot serialized - with the compact representation and `compat` is True. + with the compact representation and `compact` is True. :raises InvalidJWSSignature: if no signature has been added to the object, or no valid signature can be found. """ diff -Nru python-jwcrypto-0.6.0/jwcrypto/jwt.py python-jwcrypto-0.8.0/jwcrypto/jwt.py --- python-jwcrypto-0.6.0/jwcrypto/jwt.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/jwcrypto/jwt.py 2020-08-17 19:48:18.000000000 +0000 @@ -108,6 +108,7 @@ super(JWTInvalidClaimFormat, self).__init__(msg) +# deprecated and not used anymore class JWTMissingKeyID(JWException): """Json Web Token is missing key id. @@ -161,7 +162,7 @@ the token. A (:class:`jwcrypto.jwk.JWKSet`) can also be used. :param algs: An optional list of allowed algorithms :param default_claims: An optional dict with default values for - registred claims. A None value for NumericDate type claims + registered claims. A None value for NumericDate type claims will cause generation according to system time. Only the values from RFC 7519 - 4.1 are evaluated. :param check_claims: An optional dict of claims that must be @@ -170,7 +171,7 @@ Note: either the header,claims or jwt,key parameters should be provided as a deserialization operation (which occurs if the jwt - is provided will wipe any header os claim provided by setting + is provided) will wipe any header or claim provided by setting those obtained from the deserialization of the jwt token. Note: if check_claims is not provided the 'exp' and 'nbf' claims @@ -187,6 +188,7 @@ self._check_claims = None self._leeway = 60 # 1 minute clock skew allowed self._validity = 600 # 10 minutes validity (up to 11 with leeway) + self.deserializelog = None if header: self.header = header @@ -256,8 +258,8 @@ return self._leeway @leeway.setter - def leeway(self, l): - self._leeway = int(l) + def leeway(self, lwy): + self._leeway = int(lwy) @property def validity(self): @@ -416,12 +418,14 @@ Creates a JWS token with the header as the JWS protected header and the claims as the payload. See (:class:`jwcrypto.jws.JWS`) for - details on the exceptions that may be reaised. + details on the exceptions that may be raised. :param key: A (:class:`jwcrypto.jwk.JWK`) key. """ t = JWS(self.claims) + if self._algs: + t.allowed_algs = self._algs t.add_signature(key, protected=self.header) self.token = t @@ -430,7 +434,7 @@ Creates a JWE token with the header as the JWE protected header and the claims as the plaintext. See (:class:`jwcrypto.jwe.JWE`) for - details on the exceptions that may be reaised. + details on the exceptions that may be raised. :param key: A (:class:`jwcrypto.jwk.JWK`) key. """ @@ -462,28 +466,37 @@ if self._algs: self.token.allowed_algs = self._algs + self.deserializelog = list() # now deserialize and also decrypt/verify (or raise) if we # have a key if key is None: self.token.deserialize(jwt, None) elif isinstance(key, JWK): self.token.deserialize(jwt, key) + self.deserializelog.append("Success") elif isinstance(key, JWKSet): self.token.deserialize(jwt, None) - if 'kid' not in self.token.jose_header: - raise JWTMissingKeyID('No key ID in JWT header') - - token_key = key.get_key(self.token.jose_header['kid']) - if not token_key: - raise JWTMissingKey('Key ID %s not in key set' - % self.token.jose_header['kid']) - - if isinstance(self.token, JWE): - self.token.decrypt(token_key) - elif isinstance(self.token, JWS): - self.token.verify(token_key) + if 'kid' in self.token.jose_header: + kid_key = key.get_key(self.token.jose_header['kid']) + if not kid_key: + raise JWTMissingKey('Key ID %s not in key set' + % self.token.jose_header['kid']) + self.token.deserialize(jwt, kid_key) else: - raise RuntimeError("Unknown Token Type") + for k in key: + try: + self.token.deserialize(jwt, k) + self.deserializelog.append("Success") + break + except Exception as e: # pylint: disable=broad-except + keyid = k.key_id + if keyid is None: + keyid = k.thumbprint() + self.deserializelog.append('Key [%s] failed: [%s]' % ( + keyid, repr(e))) + continue + if "Success" not in self.deserializelog: + raise JWTMissingKey('No working key found in key set') else: raise ValueError("Unrecognized Key Type") @@ -500,7 +513,7 @@ Note: the compact parameter is provided for general compatibility with the serialize() functions of :class:`jwcrypto.jws.JWS` and :class:`jwcrypto.jwe.JWE` so that these objects can all be used - interchangeably. However the only valid JWT representtion is the + interchangeably. However the only valid JWT representation is the compact representation. """ return self.token.serialize(compact) diff -Nru python-jwcrypto-0.6.0/jwcrypto/tests.py python-jwcrypto-0.8.0/jwcrypto/tests.py --- python-jwcrypto-0.6.0/jwcrypto/tests.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/jwcrypto/tests.py 2020-08-17 19:48:18.000000000 +0000 @@ -14,6 +14,8 @@ from jwcrypto import jwk from jwcrypto import jws from jwcrypto import jwt +from jwcrypto.common import InvalidJWSERegOperation +from jwcrypto.common import JWSEHeaderParameter from jwcrypto.common import base64url_decode, base64url_encode from jwcrypto.common import json_decode, json_encode @@ -234,6 +236,62 @@ PublicCertThumbprint = u'7KITkGJF74IZ9NKVvHfuJILbuIZny6j-roaNjB1vgiA' +# RFC 8037 - A.2 +PublicKeys_EdDsa = { + "keys": [ + { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }, + ], + "thumbprints": ["kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k"] +} + +# RFC 8037 - A.1 +PrivateKeys_EdDsa = { + "keys": [ + { + "kty": "OKP", + "crv": "Ed25519", + "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}, + ] +} + +PublicKeys_secp256k1 = { + "keys": [ + { + "kty": "EC", + "crv": "secp256k1", + "x": "Ss6na3mcci8Ud4lQrjaB_T40sfKApEcl2RLIWOJdjow", + "y": "7l9qIKtKPW6oEiOYBt7r22Sm0mtFJU-yBkkvMvpscd8" + } + ] +} + +PrivateKeys_secp256k1 = { + "keys": [ + { + "kty": "EC", + "crv": "secp256k1", + "x": "Ss6na3mcci8Ud4lQrjaB_T40sfKApEcl2RLIWOJdjow", + "y": "7l9qIKtKPW6oEiOYBt7r22Sm0mtFJU-yBkkvMvpscd8", + "d": "GYhU2vrYGZrjLZn71Xniqm54Mi53xiYtaTLawzaf9dA" + }, + ] +} + +Ed25519PrivatePEM = b"""-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIEh4ImJiiZgSNg9J9I+Z5toHKh6LDO2MCbSYNZTkMXDU +-----END PRIVATE KEY----- +""" + +Ed25519PublicPEM = b"""-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAlsRcb1mVVIUcDjNqZU27N+iPXihH1EQDa/O3utHLtqc= +-----END PUBLIC KEY----- +""" + class TestJWK(unittest.TestCase): def test_create_pubKeys(self): @@ -294,6 +352,14 @@ # New param prevails key = jwk.JWK.generate(kty='EC', curve='P-256', crv='P-521') key.get_curve('P-521') + # New secp256k curve + key = jwk.JWK.generate(kty='EC', curve='secp256k1') + key.get_curve('secp256k1') + + def test_generate_OKP_keys(self): + for crv in jwk.ImplementedOkpCurves: + key = jwk.JWK.generate(kty='OKP', crv=crv) + self.assertEqual(key.get_curve(crv), crv) def test_import_pyca_keys(self): rsa1 = rsa.generate_private_key(65537, 1024, default_backend()) @@ -334,7 +400,7 @@ ks3 = jwk.JWKSet.from_json(ks.export()) self.assertEqual(len(ks), len(ks3)) - # Test Keyset with mutiple keys + # Test key set with mutiple keys ksm = jwk.JWKSet.from_json(json_encode(PrivateKeys)) num = 0 for item in ksm: @@ -392,6 +458,21 @@ self.assertFalse(pubkey.has_private) self.assertEqual(prikey.key_id, pubkey.key_id) + def test_export_as_dict(self): + key = jwk.JWK(**SymmetricKeys['keys'][1]) + k = key.export_symmetric(as_dict=True) + self.assertEqual(k['kid'], SymmetricKeys['keys'][1]['kid']) + key = jwk.JWK.from_pem(PublicCert) + k = key.export_public(as_dict=True) + self.assertEqual(k['kid'], PublicCertThumbprint) + key = jwk.JWK.from_pem(RSAPrivatePEM, password=RSAPrivatePassword) + k = key.export_private(as_dict=True) + self.assertEqual(k['kid'], + u'x31vrbZceU2qOPLtrUwPkLa3PNakMn9tOsq_ntFVrJc') + keyset = jwk.JWKSet.from_json(json_encode(PrivateKeys)) + ks = keyset.export(as_dict=True) + self.assertTrue('keys' in ks) + def test_public(self): key = jwk.JWK.from_pem(RSAPrivatePEM, password=RSAPrivatePassword) self.assertTrue(key.has_public) @@ -411,6 +492,49 @@ with self.assertRaises(jwk.InvalidJWKValue): jwk.JWK(kty='oct', k=b'\x01') + def test_create_pubKeys_eddsa(self): + keylist = PublicKeys_EdDsa['keys'] + for key in keylist: + jwk.JWK(**key) + + def test_create_priKeys_eddsa(self): + keylist = PrivateKeys_EdDsa['keys'] + for key in keylist: + jwk.JWK(**key) + + def test_create_pubKeys_secp256k1(self): + keylist = PublicKeys_secp256k1['keys'] + for key in keylist: + jwk.JWK(**key) + + def test_create_priKeys_secp256k1(self): + keylist = PrivateKeys_secp256k1['keys'] + for key in keylist: + jwk.JWK(**key) + + def test_thumbprint_eddsa(self): + for i in range(0, len(PublicKeys_EdDsa['keys'])): + k = jwk.JWK(**PublicKeys_EdDsa['keys'][i]) + self.assertEqual( + k.thumbprint(), + PublicKeys_EdDsa['thumbprints'][i]) + + def test_pem_okp(self): + payload = b'Imported private Ed25519' + prikey = jwk.JWK.from_pem(Ed25519PrivatePEM) + self.assertTrue(prikey.has_private) + self.assertTrue(prikey.has_public) + s = jws.JWS(payload) + s.add_signature(prikey, None, {'alg': 'EdDSA'}, None) + sig = s.serialize() + pubkey = jwk.JWK.from_pem(Ed25519PublicPEM) + self.assertTrue(pubkey.has_public) + self.assertFalse(pubkey.has_private) + c = jws.JWS() + c.deserialize(sig, pubkey, alg="EdDSA") + self.assertTrue(c.objects['valid']) + self.assertEqual(c.payload, payload) + # RFC 7515 - A.1 A1_protected = \ @@ -613,6 +737,20 @@ 'ZJTkVEIl0sDQogImh0dHA6Ly9leGFtcGxlLmNvbS9VTkRFRklORUQiOnRydWUNCn0.' + \ 'RkFJTA.' +customhdr_jws_example = \ + '{' + \ + '"payload":' + \ + '"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF' + \ + 'tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",' + \ + '"protected":"eyJhbGciOiJFUzI1NiJ9",' + \ + '"header":' + \ + '{"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d", ' + \ + '"custom1":"custom_val"},' + \ + '"signature":' + \ + '"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS' + \ + 'lSApmWQxfKTUJqPP3-Kg6NU1Q"' + \ + '}' + class TestJWS(unittest.TestCase): def check_sign(self, test): @@ -651,8 +789,7 @@ self.check_sign, A5_example) a5_bis = {'allowed_algs': ['none']} a5_bis.update(A5_example) - with self.assertRaises(jws.InvalidJWSSignature): - self.check_sign(a5_bis) + self.check_sign(a5_bis) def test_A6(self): s = jws.JWS(A6_example['payload']) @@ -679,6 +816,66 @@ jws.InvalidJWSSignature(s.deserialize, E_negative) s.verify(None) + def test_customhdr_jws(self): + # Test pass header check + def jws_chk1(jwobj): + return jwobj.jose_header['custom1'] == 'custom_val' + + newhdr = JWSEHeaderParameter('Custom header 1', False, True, jws_chk1) + newreg = {'custom1': newhdr} + s = jws.JWS(A6_example['payload'], header_registry=newreg) + s.deserialize(customhdr_jws_example, A6_example['key2']) + + # Test fail header check + def jws_chk2(jwobj): + return jwobj.jose_header['custom1'] == 'custom_not' + + newhdr = JWSEHeaderParameter('Custom header 1', False, True, jws_chk2) + newreg = {'custom1': newhdr} + s = jws.JWS(A6_example['payload'], header_registry=newreg) + with self.assertRaises(jws.InvalidJWSSignature): + s.deserialize(customhdr_jws_example, A6_example['key2']) + + def test_customhdr_jws_exists(self): + newhdr = JWSEHeaderParameter('Custom header 1', False, True, None) + newreg = {'alg': newhdr} + with self.assertRaises(InvalidJWSERegOperation): + jws.JWS(A6_example['payload'], header_registry=newreg) + + def test_EdDsa_signing_and_verification(self): + examples = [] + if 'Ed25519' in jwk.ImplementedOkpCurves: + examples = [E_Ed25519] + for curve_example in examples: + key = jwk.JWK.from_json(curve_example['key_json']) + payload = curve_example['payload'] + protected_header = curve_example['protected_header'] + jws_test = jws.JWS(payload) + jws_test.add_signature(key, None, + json_encode(protected_header), None) + jws_test_serialization_compact = \ + jws_test.serialize(compact=True) + self.assertEqual(jws_test_serialization_compact, + curve_example['jws_serialization_compact']) + jws_verify = jws.JWS() + jws_verify.deserialize(jws_test_serialization_compact) + jws_verify.verify(key.public()) + self.assertEqual(jws_verify.payload.decode('utf-8'), + curve_example['payload']) + + def test_secp256k1_signing_and_verification(self): + key = jwk.JWK(**PrivateKeys_secp256k1['keys'][0]) + payload = bytes(bytearray(A1_payload)) + jws_test = jws.JWS(payload) + jws_test.allowed_algs = ['ES256K'] + jws_test.add_signature(key, None, json_encode({"alg": "ES256K"}), None) + jws_test_serialization_compact = jws_test.serialize(compact=True) + jws_verify = jws.JWS() + jws_verify.allowed_algs = ['ES256K'] + jws_verify.deserialize(jws_test_serialization_compact) + jws_verify.verify(key.public()) + self.assertEqual(jws_verify.payload, payload) + E_A1_plaintext = \ [84, 104, 101, 32, 116, 114, 117, 101, 32, 115, 105, 103, 110, 32, @@ -839,6 +1036,16 @@ '"ciphertext":"KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",' \ '"tag":"Mz-VPPyU4RlcuYv1IwIvzw"}' +customhdr_jwe_ex = \ + '{"protected":"eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",' \ + '"unprotected":{"jku":"https://server.example.com/keys.jwks"},' \ + '"header":{"alg":"A128KW","kid":"7", "custom1":"custom_val"},' \ + '"encrypted_key":' \ + '"6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ",' \ + '"iv":"AxY8DCtDaGlsbGljb3RoZQ",' \ + '"ciphertext":"KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",' \ + '"tag":"Mz-VPPyU4RlcuYv1IwIvzw"}' + Issue_136_Protected_Header_no_epk = { "alg": "ECDH-ES+A256KW", "enc": "A256CBC-HS512"} @@ -862,6 +1069,23 @@ "x": "FPrb_xwxe8SBP3kO-e-WsofFp7n5-yc_tGgfAvqAP8g", "y": "lM3HuyKMYUVsYdGqiWlkwTZbGO3Fh-hyadq8lfkTgBc"} +# RFC 8037 A.1 +E_Ed25519 = { + 'key_json': '{"kty": "OKP",' + '"crv": "Ed25519", ' + '"d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", ' + '"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}', + 'payload': 'Example of Ed25519 signing', + 'protected_header': {"alg": "EdDSA"}, + 'jws_serialization_compact': 'eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBF' + 'ZDI1NTE5IHNpZ25pbmc.hgyY0il_MGCjP0Jzl' + 'nLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki' + '4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg'} + +X25519_Protected_Header_no_epk = { + "alg": "ECDH-ES+A128KW", + "enc": "A128GCM"} + class TestJWE(unittest.TestCase): def check_enc(self, plaintext, protected, key, vector): @@ -938,6 +1162,42 @@ e.deserialize(Issue_136_Contributed_JWE, jwk.JWK(**Issue_136_Contributed_Key)) + def test_customhdr_jwe(self): + def jwe_chk1(jwobj): + return jwobj.jose_header['custom1'] == 'custom_val' + + newhdr = JWSEHeaderParameter('Custom header 1', False, True, jwe_chk1) + newreg = {'custom1': newhdr} + e = jwe.JWE(header_registry=newreg) + e.deserialize(customhdr_jwe_ex, E_A4_ex['key2']) + + def jwe_chk2(jwobj): + return jwobj.jose_header['custom1'] == 'custom_not' + + newhdr = JWSEHeaderParameter('Custom header 1', False, True, jwe_chk2) + newreg = {'custom1': newhdr} + e = jwe.JWE(header_registry=newreg) + with self.assertRaises(jwe.InvalidJWEData): + e.deserialize(customhdr_jwe_ex, E_A4_ex['key2']) + + def test_customhdr_jwe_exists(self): + newhdr = JWSEHeaderParameter('Custom header 1', False, True, None) + newreg = {'alg': newhdr} + with self.assertRaises(InvalidJWSERegOperation): + jwe.JWE(header_registry=newreg) + + def test_X25519_ECDH(self): + plaintext = b"plain" + protected = json_encode(X25519_Protected_Header_no_epk) + if 'X25519' in jwk.ImplementedOkpCurves: + x25519key = jwk.JWK.generate(kty='OKP', crv='X25519') + e1 = jwe.JWE(plaintext, protected) + e1.add_recipient(x25519key) + enc = e1.serialize() + e2 = jwe.JWE() + e2.deserialize(enc, x25519key) + self.assertEqual(e2.payload, plaintext) + MMA_vector_key = jwk.JWK(**E_A2_key) MMA_vector_ok_cek = \ @@ -1094,20 +1354,35 @@ def test_decrypt_keyset(self): key = jwk.JWK(kid='testkey', **E_A2_key) - keyset = jwk.JWKSet() - # decrypt without keyid - t = jwt.JWT(A1_header, A1_claims) + keyset = jwk.JWKSet.from_json(json_encode(PrivateKeys)) + + # encrypt a new JWT with kid + header = copy.copy(A1_header) + header['kid'] = 'testkey' + t = jwt.JWT(header, A1_claims) + t.make_encrypted_token(key) + token = t.serialize() + # try to decrypt without a matching key + self.assertRaises(jwt.JWTMissingKey, jwt.JWT, jwt=token, key=keyset) + # now decrypt with key + keyset.add(key) + jwt.JWT(jwt=token, key=keyset, check_claims={'exp': 1300819380}) + + # encrypt a new JWT with wrong kid + header = copy.copy(A1_header) + header['kid'] = '1' + t = jwt.JWT(header, A1_claims) t.make_encrypted_token(key) token = t.serialize() - self.assertRaises(jwt.JWTMissingKeyID, jwt.JWT, jwt=token, - key=keyset) - # encrypt a new JWT + self.assertRaises(jwe.InvalidJWEData, jwt.JWT, jwt=token, key=keyset) + + keyset = jwk.JWKSet.from_json(json_encode(PrivateKeys)) + # encrypt a new JWT with no kid header = copy.copy(A1_header) - header['kid'] = 'testkey' t = jwt.JWT(header, A1_claims) t.make_encrypted_token(key) token = t.serialize() - # try to decrypt without key + # try to decrypt without a matching key self.assertRaises(jwt.JWTMissingKey, jwt.JWT, jwt=token, key=keyset) # now decrypt with key keyset.add(key) @@ -1238,6 +1513,19 @@ check.deserialize(enc, key) self.assertEqual(b'plain', check.payload) + def test_none_key(self): + e = "eyJhbGciOiJub25lIn0." + \ + "eyJpc3MiOiJqb2UiLCJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZX0." + token = jwt.JWT(algs=['none']) + k = jwk.JWK(generate='oct', size=0) + token.deserialize(jwt=e, key=k) + self.assertEqual(json_decode(token.claims), + {"iss": "joe", "http://example.com/is_root": True}) + with self.assertRaises(KeyError): + token = jwt.JWT() + token.deserialize(jwt=e) + json_decode(token.claims) + class JWATests(unittest.TestCase): def test_jwa_create(self): @@ -1246,6 +1534,8 @@ self.assertIn(cls.algorithm_usage_location, {'alg', 'enc'}) if name == 'ECDH-ES': self.assertIs(cls.keysize, None) + elif name == 'EdDSA': + self.assertIs(cls.keysize, None) else: self.assertIsInstance(cls.keysize, int) self.assertGreaterEqual(cls.keysize, 0) diff -Nru python-jwcrypto-0.6.0/README.md python-jwcrypto-0.8.0/README.md --- python-jwcrypto-0.6.0/README.md 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/README.md 2020-08-17 19:48:18.000000000 +0000 @@ -4,13 +4,13 @@ ======== An implementation of the JOSE Working Group documents: -RFC 7515 - JSON Web Signature (JWS) -RFC 7516 - JSON Web Encryption (JWE) -RFC 7517 - JSON Web Key (JWK) -RFC 7518 - JSON Web Algorithms (JWA) -RFC 7519 - JSON Web Token (JWT) -RFC 7520 - Examples of Protecting Content Using JSON Object Signing and -Encryption (JOSE) +- RFC 7515 - JSON Web Signature (JWS) +- RFC 7516 - JSON Web Encryption (JWE) +- RFC 7517 - JSON Web Key (JWK) +- RFC 7518 - JSON Web Algorithms (JWA) +- RFC 7519 - JSON Web Token (JWT) +- RFC 7520 - Examples of Protecting Content Using JSON Object Signing and + Encryption (JOSE) Documentation ============= diff -Nru python-jwcrypto-0.6.0/setup.py python-jwcrypto-0.8.0/setup.py --- python-jwcrypto-0.6.0/setup.py 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/setup.py 2020-08-17 19:48:18.000000000 +0000 @@ -6,7 +6,7 @@ setup( name = 'jwcrypto', - version = '0.6.0', + version = '0.8', license = 'LGPLv3+', maintainer = 'JWCrypto Project Contributors', maintainer_email = 'simo@redhat.com', @@ -19,12 +19,13 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Intended Audience :: Developers', 'Topic :: Security', 'Topic :: Software Development :: Libraries :: Python Modules' ], data_files = [('share/doc/jwcrypto', ['LICENSE', 'README.md'])], install_requires = [ - 'cryptography >= 1.5', + 'cryptography >= 2.3', ], ) diff -Nru python-jwcrypto-0.6.0/tox.ini python-jwcrypto-0.8.0/tox.ini --- python-jwcrypto-0.6.0/tox.ini 2018-11-05 15:14:47.000000000 +0000 +++ python-jwcrypto-0.8.0/tox.ini 2020-08-17 19:48:18.000000000 +0000 @@ -8,7 +8,7 @@ deps = pytest coverage -sitepackages = True +#sitepackages = True commands = {envpython} -bb -m coverage run -m pytest --capture=no --strict {posargs} {envpython} -m coverage report -m @@ -17,7 +17,7 @@ basepython = python2.7 deps = pylint -sitepackages = True +#sitepackages = True commands = {envpython} -m pylint -d c,r,i,W0613 -r n -f colorized --notes= --disable=star-args ./jwcrypto @@ -50,10 +50,10 @@ markdown_py README.md -f {toxworkdir}/README.md.html [testenv:sphinx] -basepython = python2.7 +basepython = python3 changedir = docs/source deps = - sphinx < 1.3.0 + sphinx commands = sphinx-build -v -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html