Hello community, here is the log from the commit of package python-jwcrypto for openSUSE:Leap:15.2 checked in at 2020-04-20 12:55:47 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Leap:15.2/python-jwcrypto (Old) and /work/SRC/openSUSE:Leap:15.2/.python-jwcrypto.new.2738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-jwcrypto" Mon Apr 20 12:55:47 2020 rev:12 rq:795595 version:0.7 Changes: -------- --- /work/SRC/openSUSE:Leap:15.2/python-jwcrypto/python-jwcrypto.changes 2020-03-09 18:07:09.148870741 +0100 +++ /work/SRC/openSUSE:Leap:15.2/.python-jwcrypto.new.2738/python-jwcrypto.changes 2020-04-20 12:55:56.960771854 +0200 @@ -1,0 +2,15 @@ +Mon Mar 30 08:15:59 UTC 2020 - Michael Ströder <mich...@stroeder.com> + +- update to upstream release 0.7.0 + * Allow to use JWKSet on a JWT with no KID + * Fixed JWE jose_header + * Added JWE/JWS custom registry header implementation + * RFC 8037 - Support for Ed25519, Ed448 + * Stricter OKP key generation parms check + * Add X25519/X448 support + * Simplify internal code curve selection + * Fix encoding length of EC keys Coordinates + * Add the ability to verify 'none' signatures + * Import ABC from collections.abc instead of collections for Python 3.9 compatibility + +------------------------------------------------------------------- Old: ---- jwcrypto-0.6.0.tar.gz New: ---- jwcrypto-0.7.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-jwcrypto.spec ++++++ --- /var/tmp/diff_new_pack.ErxoKS/_old 2020-04-20 12:55:57.328772431 +0200 +++ /var/tmp/diff_new_pack.ErxoKS/_new 2020-04-20 12:55:57.332772437 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-jwcrypto -Version: 0.6.0 +Version: 0.7 Release: 0 Summary: Python module package implementing JOSE Web standards License: LGPL-3.0-only ++++++ jwcrypto-0.6.0.tar.gz -> jwcrypto-0.7.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/PKG-INFO new/jwcrypto-0.7/PKG-INFO --- old/jwcrypto-0.6.0/PKG-INFO 2018-11-05 16:18:50.000000000 +0100 +++ new/jwcrypto-0.7/PKG-INFO 2020-02-19 18:17:34.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: jwcrypto -Version: 0.6.0 +Version: 0.7 Summary: Implementation of JOSE Web standards Home-page: https://github.com/latchset/jwcrypto Maintainer: JWCrypto Project Contributors diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/jwcrypto/common.py new/jwcrypto-0.7/jwcrypto/common.py --- old/jwcrypto-0.6.0/jwcrypto/common.py 2018-06-27 08:24:18.000000000 +0200 +++ new/jwcrypto-0.7/jwcrypto/common.py 2020-02-19 17:12:20.000000000 +0100 @@ -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): @@ -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 sepcified 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/jwcrypto/jwa.py new/jwcrypto-0.7/jwcrypto/jwa.py --- old/jwcrypto-0.6.0/jwcrypto/jwa.py 2018-11-05 16:04:15.000000000 +0100 +++ new/jwcrypto-0.7/jwcrypto/jwa.py 2020-02-19 17:12:20.000000000 +0100 @@ -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): @@ -693,8 +694,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 +723,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 +813,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): @@ -1026,6 +1059,7 @@ 'ECDH-ES+A128KW': _EcdhEsAes128Kw, 'ECDH-ES+A192KW': _EcdhEsAes192Kw, 'ECDH-ES+A256KW': _EcdhEsAes256Kw, + 'EdDSA': _EdDsa, 'A128GCMKW': _A128GcmKw, 'A192GCMKW': _A192GcmKw, 'A256GCMKW': _A256GcmKw, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/jwcrypto/jwe.py new/jwcrypto-0.7/jwcrypto/jwe.py --- old/jwcrypto-0.6.0/jwcrypto/jwe.py 2018-11-05 16:04:15.000000000 +0100 +++ new/jwcrypto-0.7/jwcrypto/jwe.py 2019-05-27 14:17:28.000000000 +0200 @@ -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 @@ -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)) @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/jwcrypto/jwk.py new/jwcrypto-0.7/jwcrypto/jwk.py --- old/jwcrypto-0.6.0/jwcrypto/jwk.py 2018-11-05 16:04:15.000000000 +0100 +++ new/jwcrypto-0.7/jwcrypto/jwk.py 2020-02-19 17:12:20.000000000 +0100 @@ -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""" @@ -79,10 +150,14 @@ } """Regstry of valid key parameters""" -# RFC 7518 - 7.6 +# RFC 7518 - 7.6 , RFC 8037 - 5 JWKEllipticCurveRegistry = {'P-256': 'P-256 curve', 'P-384': 'P-384 curve', - 'P-521': 'P-521 curve'} + 'P-521': 'P-521 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 @@ -212,7 +287,8 @@ 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) + * OKP: crv(str) (one of Ed25519, Ed448, X25519, X448) Deprecated: Alternatively if the 'generate' parameter is provided, with a @@ -274,9 +350,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 +401,8 @@ return ec.SECP384R1() elif name == 'P-521': return ec.SECP521R1() + elif name in _OKP_CURVES_TABLE: + return name else: raise InvalidJWKValue('Unknown Elliptic Curve Type') @@ -334,12 +420,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 +440,40 @@ ) 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 _import_pyca_pri_okp(self, key, **params): + params.update( + kty='OKP', + crv=params['crv'], + 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=params['crv'], + x=base64url_encode(key.public_bytes( + serialization.Encoding.Raw, + serialization.PrivateFormat.Raw)) + ) + self.import_key(**params) + def import_key(self, **kwargs): names = list(kwargs.keys()) @@ -385,7 +506,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]) @@ -547,8 +668,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 +677,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 +726,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 +749,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,6 +761,8 @@ 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 @@ -673,6 +814,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) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/jwcrypto/jws.py new/jwcrypto-0.7/jwcrypto/jws.py --- old/jwcrypto-0.6.0/jwcrypto/jws.py 2018-11-05 16:04:15.000000000 +0100 +++ new/jwcrypto-0.7/jwcrypto/jws.py 2020-02-19 17:12:20.000000000 +0100 @@ -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') @@ -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, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/jwcrypto/jwt.py new/jwcrypto-0.7/jwcrypto/jwt.py --- old/jwcrypto-0.6.0/jwcrypto/jwt.py 2018-11-05 16:04:15.000000000 +0100 +++ new/jwcrypto-0.7/jwcrypto/jwt.py 2019-05-27 14:17:28.000000000 +0200 @@ -108,6 +108,7 @@ super(JWTInvalidClaimFormat, self).__init__(msg) +# deprecated and not used anymore class JWTMissingKeyID(JWException): """Json Web Token is missing key id. @@ -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 @@ -462,28 +464,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") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/jwcrypto/tests.py new/jwcrypto-0.7/jwcrypto/tests.py --- old/jwcrypto-0.6.0/jwcrypto/tests.py 2018-11-05 16:04:15.000000000 +0100 +++ new/jwcrypto-0.7/jwcrypto/tests.py 2020-02-19 17:12:20.000000000 +0100 @@ -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,29 @@ 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"}, + ] +} + class TestJWK(unittest.TestCase): def test_create_pubKeys(self): @@ -295,6 +320,11 @@ key = jwk.JWK.generate(kty='EC', curve='P-256', crv='P-521') key.get_curve('P-521') + 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()) krsa1 = jwk.JWK.from_pyca(rsa1) @@ -411,6 +441,23 @@ 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_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]) + # RFC 7515 - A.1 A1_protected = \ @@ -613,6 +660,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 +712,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 +739,53 @@ 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']) + E_A1_plaintext = \ [84, 104, 101, 32, 116, 114, 117, 101, 32, 115, 105, 103, 110, 32, @@ -839,6 +946,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 +979,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 +1072,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 +1264,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() - self.assertRaises(jwt.JWTMissingKeyID, jwt.JWT, jwt=token, - key=keyset) - # encrypt a new JWT + # 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'] = 'testkey' + header['kid'] = '1' t = jwt.JWT(header, A1_claims) t.make_encrypted_token(key) token = t.serialize() - # try to decrypt without key + 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) + 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) @@ -1238,6 +1423,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 +1444,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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/jwcrypto.egg-info/PKG-INFO new/jwcrypto-0.7/jwcrypto.egg-info/PKG-INFO --- old/jwcrypto-0.6.0/jwcrypto.egg-info/PKG-INFO 2018-11-05 16:18:50.000000000 +0100 +++ new/jwcrypto-0.7/jwcrypto.egg-info/PKG-INFO 2020-02-19 18:17:34.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: jwcrypto -Version: 0.6.0 +Version: 0.7 Summary: Implementation of JOSE Web standards Home-page: https://github.com/latchset/jwcrypto Maintainer: JWCrypto Project Contributors diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/setup.py new/jwcrypto-0.7/setup.py --- old/jwcrypto-0.6.0/setup.py 2018-11-05 16:13:11.000000000 +0100 +++ new/jwcrypto-0.7/setup.py 2020-02-19 18:15:54.000000000 +0100 @@ -6,7 +6,7 @@ setup( name = 'jwcrypto', - version = '0.6.0', + version = '0.7', license = 'LGPLv3+', maintainer = 'JWCrypto Project Contributors', maintainer_email = 's...@redhat.com', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jwcrypto-0.6.0/tox.ini new/jwcrypto-0.7/tox.ini --- old/jwcrypto-0.6.0/tox.ini 2018-06-27 12:24:24.000000000 +0200 +++ new/jwcrypto-0.7/tox.ini 2020-02-19 17:12:20.000000000 +0100 @@ -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