Skip to content
Commits on Source (64)
......@@ -12,15 +12,17 @@ matrix:
env: TOXENV=py34
- python: 3.5
env: TOXENV=py35
- python: 3.5
- python: 3.6
env: TOXENV=py36
- python: 3.6
env: TOXENV=doc
- python: 3.5
- python: 3.6
env: TOXENV=sphinx
- python: 3.5
- python: 3.6
env: TOXENV=lint
- python: 2.7
env: TOXENV=pep8py2
- python: 3.5
- python: 3.6
env: TOXENV=pep8py3
install:
......
include LICENSE README.md
include tox.ini setup.cfg
......@@ -17,11 +17,19 @@ clean:
cscope:
git ls-files | xargs pycscope
testlong: export JWCRYPTO_TESTS_ENABLE_MMA=True
testlong: export TOX_TESTENV_PASSENV=JWCRYPTO_TESTS_ENABLE_MMA
testlong:
rm -f .coverage
tox -e py36
test:
rm -f .coverage
tox -e py27
tox -e py34 --skip-missing-interpreter
tox -e py35 --skip-missing-interpreter
tox -e py36 --skip-missing-interpreter
tox -e py37 --skip-missing-interpreter
DOCS_DIR = docs
.PHONY: docs
......
[![Build Status](https://travis-ci.org/latchset/jwcrypto.svg?branch=master)](https://travis-ci.org/latchset/jwcrypto)
JWCrypto
========
......
......@@ -46,16 +46,16 @@ master_doc = 'index'
# General information about the project.
project = u'JWCrypto'
copyright = u'2016, JWCrypto Contributors'
copyright = u'2016-2018, JWCrypto Contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.3'
version = '0.6'
# The full version, including alpha/beta/rc tags.
release = '0.3.1'
release = '0.6'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
......
......@@ -47,3 +47,52 @@ Registries
.. autodata:: jwcrypto.jwe.JWEHeaderRegistry
:annotation:
Examples
--------
Symmetric keys
~~~~~~~~~~~~~~
Encrypt a JWE token::
>>> from jwcrypto import jwk, jwe
>>> from jwcrypto.common import json_encode
>>> key = jwk.JWK.generate(kty='oct', size=256)
>>> payload = "My Encrypted message"
>>> jwetoken = jwe.JWE(payload.encode('utf-8'),
json_encode({"alg": "A256KW",
"enc": "A256CBC-HS512"}))
>>> jwetoken.add_recipient(key)
>>> enc = jwetoken.serialize()
Decrypt a JWE token::
>>> jwetoken = jwe.JWE()
>>> jwetoken.deserialize(enc)
>>> jwetoken.decrypt(key)
>>> payload = jwetoken.payload
Asymmetric keys
~~~~~~~~~~~~~~~
Encrypt a JWE token::
>>> from jwcrypto import jwk, jwe
>>> from jwcrypto.common import json_encode, json_decode
>>> public_key = jwk.JWK()
>>> private_key = jwk.JWK.generate(kty='RSA', size=2048)
>>> public_key.import_key(**json_decode(private_key.export_public()))
>>> payload = "My Encrypted message"
>>> protected_header = {
"alg": "RSA-OAEP-256",
"enc": "A256CBC-HS512",
"typ": "JWE",
"kid": public_key.thumbprint(),
}
>>> jwetoken = jwe.JWE(payload.encode('utf-8'),
recipient=public_key,
protected=protected_header)
>>> enc = jwetoken.serialize()
Decrypt a JWE token::
>>> jwetoken = jwe.JWE()
>>> jwetoken.deserialize(enc, key=private_key)
>>> payload = jwetoken.payload
......@@ -85,3 +85,6 @@ Import a P-256 Public Key::
"crv":"P-256","kty":"EC"}
>>> key = jwk.JWK(**expkey)
Import a Key from a PEM file::
>>> with open("public.pem", "rb") as pemfile:
>>> key = jwk.JWK.from_pem(pemfile.read())
......@@ -43,3 +43,23 @@ Registries
.. autodata:: jwcrypto.jws.JWSHeaderRegistry
:annotation:
Examples
--------
Sign a JWS token::
>>> from jwcrypto import jwk, jws
>>> from jwcrypto.common import json_encode
>>> key = jwk.JWK.generate(kty='oct', size=256)
>>> payload = "My Integrity protected message"
>>> jwstoken = jws.JWS(payload.encode('utf-8'))
>>> jwstoken.add_signature(key, None,
json_encode({"alg": "HS256"}),
json_encode({"kid": key.thumbprint()}))
>>> sig = jwstoken.serialize()
Verify a JWS token::
>>> jwstoken = jws.JWS()
>>> jwstoken.deserialize(sig)
>>> jwstoken.verify(key)
>>> payload = jwstoken.payload
......@@ -16,12 +16,12 @@ def base64url_encode(payload):
def base64url_decode(payload):
l = len(payload) % 4
if l == 2:
size = len(payload) % 4
if size == 2:
payload += '=='
elif l == 3:
elif size == 3:
payload += '='
elif l != 0:
elif size != 0:
raise ValueError('Invalid base64 string')
return urlsafe_b64decode(payload.encode('utf-8'))
......@@ -40,9 +40,67 @@ def json_decode(string):
return json.loads(string)
class InvalidJWAAlgorithm(Exception):
class JWException(Exception):
pass
class InvalidJWAAlgorithm(JWException):
def __init__(self, message=None):
msg = 'Invalid JWS Algorithm name'
msg = 'Invalid JWA Algorithm name'
if message:
msg += ' (%s)' % message
super(InvalidJWAAlgorithm, self).__init__(msg)
class InvalidCEKeyLength(JWException):
"""Invalid CEK Key Length.
This exception is raised when a Content Encryption Key does not match
the required lenght.
"""
def __init__(self, expected, obtained):
msg = 'Expected key of length %d bits, got %d' % (expected, obtained)
super(InvalidCEKeyLength, self).__init__(msg)
class InvalidJWEOperation(JWException):
"""Invalid JWS Object.
This exception is raised when a requested operation cannot
be execute due to unsatisfied conditions.
"""
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(InvalidJWEOperation, self).__init__(msg)
class InvalidJWEKeyType(JWException):
"""Invalid JWE Key Type.
This exception is raised when the provided JWK Key does not match
the type required by the sepcified algorithm.
"""
def __init__(self, expected, obtained):
msg = 'Expected key type %s, got %s' % (expected, obtained)
super(InvalidJWEKeyType, self).__init__(msg)
class InvalidJWEKeyLength(JWException):
"""Invalid JWE Key Length.
This exception is raised when the provided JWK Key does not match
the lenght required by the sepcified algorithm.
"""
def __init__(self, expected, obtained):
msg = 'Expected key of lenght %d, got %d' % (expected, obtained)
super(InvalidJWEKeyLength, self).__init__(msg)
This diff is collapsed.
This diff is collapsed.
# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
import os
from binascii import hexlify, unhexlify
from collections import namedtuple
from enum import Enum
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import rsa
from six import iteritems
from jwcrypto.common import JWException
from jwcrypto.common import base64url_decode, base64url_encode
from jwcrypto.common import json_decode, json_encode
......@@ -21,36 +24,59 @@ JWKTypesRegistry = {'EC': 'Elliptic Curve',
'oct': 'Octet sequence'}
"""Registry of valid Key Types"""
# RFC 7518 - 7.5
# It is part of the JWK Parameters Registry, but we want a more
# specific map for internal usage
JWKValuesRegistry = {'EC': {'crv': ('Curve', 'Public', 'Required'),
'x': ('X Coordinate', 'Public', 'Required'),
'y': ('Y Coordinate', 'Public', 'Required'),
'd': ('ECC Private Key', 'Private', None)},
'RSA': {'n': ('Modulus', 'Public', 'Required'),
'e': ('Exponent', 'Public', 'Required'),
'd': ('Private Exponent', 'Private', None),
'p': ('First Prime Factor', 'Private', None),
'q': ('Second Prime Factor', 'Private', None),
'dp': ('First Factor CRT Exponent', 'Private',
None),
'dq': ('Second Factor CRT Exponent', 'Private',
None),
'qi': ('First CRT Coefficient', 'Private', None)},
'oct': {'k': ('Key Value', 'Private', 'Required')}}
class ParmType(Enum):
name = 'A string with a name'
b64 = 'Base64url Encoded'
b64U = 'Base64urlUint Encoded'
unsupported = 'Unsupported Parameter'
JWKParameter = namedtuple('Parameter', 'description public required type')
JWKValuesRegistry = {
'EC': {
'crv': JWKParameter('Curve', True, True, ParmType.name),
'x': JWKParameter('X Coordinate', True, True, ParmType.b64),
'y': JWKParameter('Y Coordinate', True, True, ParmType.b64),
'd': JWKParameter('ECC Private Key', False, False, ParmType.b64),
},
'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),
'dp': JWKParameter('First Factor CRT Exponent',
False, False, ParmType.b64U),
'dq': JWKParameter('Second Factor CRT Exponent',
False, False, ParmType.b64U),
'qi': JWKParameter('First CRT Coefficient',
False, False, ParmType.b64U),
'oth': JWKParameter('Other Primes Info',
False, False, ParmType.unsupported),
},
'oct': {
'k': JWKParameter('Key Value', False, True, ParmType.b64),
}
}
"""Registry of valid key values"""
JWKParamsRegistry = {'kty': ('Key Type', 'Public', ),
'use': ('Public Key Use', 'Public'),
'key_ops': ('Key Operations', 'Public'),
'alg': ('Algorithm', 'Public'),
'kid': ('Key ID', 'Public'),
'x5u': ('X.509 URL', 'Public'),
'x5c': ('X.509 Certificate Chain', 'Public'),
'x5t': ('X.509 Certificate SHA-1 Thumbprint', 'Public'),
'x5t#S256': ('X.509 Certificate SHA-256 Thumbprint',
'Public')}
JWKParamsRegistry = {
'kty': JWKParameter('Key Type', True, None, None),
'use': JWKParameter('Public Key Use', True, None, None),
'key_ops': JWKParameter('Key Operations', True, None, None),
'alg': JWKParameter('Algorithm', True, None, None),
'kid': JWKParameter('Key ID', True, None, None),
'x5u': JWKParameter('X.509 URL', True, None, None),
'x5c': JWKParameter('X.509 Certificate Chain', True, None, None),
'x5t': JWKParameter('X.509 Certificate SHA-1 Thumbprint',
True, None, None),
'x5t#S256': JWKParameter('X.509 Certificate SHA-256 Thumbprint',
True, None, None)
}
"""Regstry of valid key parameters"""
# RFC 7518 - 7.6
......@@ -82,7 +108,7 @@ JWKpycaCurveMap = {'secp256r1': 'P-256',
'secp521r1': 'P-521'}
class InvalidJWKType(Exception):
class InvalidJWKType(JWException):
"""Invalid JWK Type Exception.
This exception is raised when an invalid parameter type is used.
......@@ -97,7 +123,7 @@ class InvalidJWKType(Exception):
self.value, list(JWKTypesRegistry.keys()))
class InvalidJWKUsage(Exception):
class InvalidJWKUsage(JWException):
"""Invalid JWK usage Exception.
This exception is raised when an invalid key usage is requested,
......@@ -122,7 +148,7 @@ class InvalidJWKUsage(Exception):
valid)
class InvalidJWKOperation(Exception):
class InvalidJWKOperation(JWException):
"""Invalid JWK Operation Exception.
This exception is raised when an invalid key operation is requested,
......@@ -149,7 +175,7 @@ class InvalidJWKOperation(Exception):
valid)
class InvalidJWKValue(Exception):
class InvalidJWKValue(JWException):
"""Invalid JWK Value Exception.
This exception is raised when an invalid/unknown value is used in the
......@@ -165,7 +191,7 @@ class JWK(object):
This object represent a Key.
It must be instantiated by using the standard defined key/value pairs
as arguents of the initialization function.
as arguments of the initialization function.
"""
def __init__(self, **kwargs):
......@@ -209,6 +235,7 @@ class JWK(object):
@classmethod
def generate(cls, **kwargs):
obj = cls()
kty = None
try:
kty = kwargs['kty']
gen = getattr(obj, '_generate_%s' % kty)
......@@ -218,20 +245,30 @@ class JWK(object):
return obj
def generate_key(self, **params):
kty = None
try:
kty = params['generate']
del params['generate']
kty = params.pop('generate')
gen = getattr(self, '_generate_%s' % kty)
except (KeyError, AttributeError):
raise InvalidJWKType(kty)
gen(params)
def _generate_oct(self, params):
size = 128
def _get_gen_size(self, params, default_size=None):
size = default_size
if 'size' in params:
size = params['size']
del params['size']
size = params.pop('size')
elif 'alg' in params:
try:
from jwcrypto.jwa import JWA
alg = JWA.instantiate_alg(params['alg'])
except KeyError:
raise ValueError("Invalid 'alg' parameter")
size = alg.keysize
return size
def _generate_oct(self, params):
size = self._get_gen_size(params, 128)
key = os.urandom(size // 8)
params['kty'] = 'oct'
params['k'] = base64url_encode(key)
......@@ -243,13 +280,9 @@ class JWK(object):
def _generate_RSA(self, params):
pubexp = 65537
size = 2048
size = self._get_gen_size(params, 2048)
if 'public_exponent' in params:
pubexp = params['public_exponent']
del params['public_exponent']
if 'size' in params:
size = params['size']
del params['size']
pubexp = params.pop('public_exponent')
key = rsa.generate_private_key(pubexp, size, default_backend())
self._import_pyca_pri_rsa(key, **params)
......@@ -290,13 +323,11 @@ class JWK(object):
def _generate_EC(self, params):
curve = 'P-256'
if 'curve' in params:
curve = params['curve']
del params['curve']
curve = params.pop('curve')
# 'curve' is for backwards compat, if 'crv' is defined it takes
# precedence
if 'crv' in params:
curve = params['crv']
del params['crv']
curve = params.pop('crv')
curve_name = self._get_curve_by_name(curve)
key = ec.generate_private_key(curve_name, default_backend())
self._import_pyca_pri_ec(key, **params)
......@@ -342,8 +373,26 @@ class JWK(object):
names.remove(name)
for name, val in iteritems(JWKValuesRegistry[kty]):
if val[2] == 'Required' and name not in self._key:
if val.required and name not in self._key:
raise InvalidJWKValue('Missing required value %s' % name)
if val.type == ParmType.unsupported and name in self._key:
raise InvalidJWKValue('Unsupported parameter %s' % name)
if val.type == ParmType.b64 and name in self._key:
# Check that the value is base64url encoded
try:
base64url_decode(self._key[name])
except Exception: # pylint: disable=broad-except
raise InvalidJWKValue(
'"%s" is not base64url encoded' % name
)
if val[3] == ParmType.b64U and name in self._key:
# Check that the value is Base64urlUInt encoded
try:
self._decode_int(self._key[name])
except Exception: # pylint: disable=broad-except
raise InvalidJWKValue(
'"%s" is not Base64urlUInt encoded' % name
)
# Unknown key parameters are allowed
# Let's just store them out of the way
......@@ -381,36 +430,107 @@ class JWK(object):
' "key_ops" values specified at'
' the same time')
@classmethod
def from_json(cls, key):
"""Creates a RFC 7517 JWK from the standard JSON format.
:param key: The RFC 7517 representation of a JWK.
"""
obj = cls()
try:
jkey = json_decode(key)
except Exception as e: # pylint: disable=broad-except
raise InvalidJWKValue(e)
obj.import_key(**jkey)
return obj
def export(self, private_key=True):
"""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.
:param private_key(bool): Whether to export the private key.
Defaults to True.
"""
if private_key is not True:
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()
d = dict()
d.update(self._params)
d.update(self._key)
d.update(self._unknown)
return json_encode(d)
def export_public(self):
"""Exports the public key in the standard JSON format.
This function is deprecated and maintained only for
backwards compatibility, use export(private_key=False)
instead."""
It fails if one is not available like when this function
is called on a symmetric key.
"""
pub = self._public_params()
return json_encode(pub)
def _public_params(self):
if not self.has_public:
raise InvalidJWKType("No public key available")
pub = {}
preg = JWKParamsRegistry
for name in preg:
if preg[name][1] == 'Public':
if preg[name].public:
if name in self._params:
pub[name] = self._params[name]
reg = JWKValuesRegistry[self._params['kty']]
for param in reg:
if reg[param][1] == 'Public':
if reg[param].public:
pub[param] = self._key[param]
return json_encode(pub)
return pub
def _export_all(self):
d = dict()
d.update(self._params)
d.update(self._key)
d.update(self._unknown)
return json_encode(d)
def export_private(self):
"""Export the private key in the standard JSON format.
It fails for a JWK that has only a public key or is symmetric.
"""
if self.has_private:
return self._export_all()
raise InvalidJWKType("No private key available")
def export_symmetric(self):
if self.is_symmetric:
return self._export_all()
raise InvalidJWKType("Not a symmetric key")
def public(self):
pub = self._public_params()
return JWK(**pub)
@property
def has_public(self):
"""Whether this JWK has an asymmetric Public key."""
if self.is_symmetric:
return False
reg = JWKValuesRegistry[self._params['kty']]
for value in reg:
if reg[value].public and value in self._key:
return True
@property
def has_private(self):
"""Whether this JWK has an asymmetric key Private key."""
if self.is_symmetric:
return False
reg = JWKValuesRegistry[self._params['kty']]
for value in reg:
if not reg[value].public and value in self._key:
return True
return False
@property
def is_symmetric(self):
"""Whether this JWK is a symmetric key."""
return self.key_type == 'oct'
@property
def key_type(self):
......@@ -556,12 +676,89 @@ class JWK(object):
else:
raise InvalidJWKValue('Unknown key object %r' % key)
def import_from_pem(self, data, password=None):
"""Imports a key from data loaded from a PEM file.
The key may be encrypted with a password.
Private keys (PKCS#8 format), public keys, and X509 certificate's
public keys can be imported with this interface.
:param data(bytes): The data contained in a PEM file.
:param password(bytes): An optional password to unwrap the key.
"""
try:
key = serialization.load_pem_private_key(
data, password=password, backend=default_backend())
except ValueError as e:
if password is not None:
raise e
try:
key = serialization.load_pem_public_key(
data, backend=default_backend())
except ValueError:
try:
cert = x509.load_pem_x509_certificate(
data, backend=default_backend())
key = cert.public_key()
except ValueError:
raise e
self.import_from_pyca(key)
self._params['kid'] = self.thumbprint()
def export_to_pem(self, private_key=False, password=False):
"""Exports keys to a data buffer suitable to be stored as a PEM file.
Either the public or the private key can be exported to a PEM file.
For private keys the PKCS#8 format is used. If a password is provided
the best encryption method available as determined by the cryptography
module is used to wrap the key.
:param private_key: Whether the private key should be exported.
Defaults to `False` which means the public key is exported by default.
:param password(bytes): A password for wrapping the private key.
Defaults to False which will cause the operation to fail. To avoid
encryption the user must explicitly pass None, otherwise the user
needs to provide a password in a bytes buffer.
"""
e = serialization.Encoding.PEM
if private_key:
if not self.has_private:
raise InvalidJWKType("No private key available")
f = serialization.PrivateFormat.PKCS8
if password is None:
a = serialization.NoEncryption()
elif isinstance(password, bytes):
a = serialization.BestAvailableEncryption(password)
elif password is False:
raise ValueError("The password must be None or a bytes string")
else:
raise TypeError("The password string must be bytes")
return self._get_private_key().private_bytes(
encoding=e, format=f, encryption_algorithm=a)
else:
if not self.has_public:
raise InvalidJWKType("No public key available")
f = serialization.PublicFormat.SubjectPublicKeyInfo
return self._get_public_key().public_bytes(encoding=e, format=f)
@classmethod
def from_pyca(cls, key):
obj = cls()
obj.import_from_pyca(key)
return obj
@classmethod
def from_pem(cls, data, password=None):
"""Creates a key from PKCS#8 formatted data loaded from a PEM file.
See the function `import_from_pem` for details.
:param data(bytes): The data contained in a PEM file.
:param password(bytes): An optional password to unwrap the key.
"""
obj = cls()
obj.import_from_pem(data, password)
return obj
def thumbprint(self, hashalg=hashes.SHA256()):
"""Returns the key thumbprint as specified by RFC 7638.
......@@ -570,7 +767,7 @@ class JWK(object):
t = {'kty': self._params['kty']}
for name, val in iteritems(JWKValuesRegistry[t['kty']]):
if val[2] == 'Required':
if val.required:
t[name] = self._key[name]
digest = hashes.Hash(hashalg, backend=default_backend())
digest.update(bytes(json_encode(t).encode('utf8')))
......@@ -603,6 +800,12 @@ class JWKSet(dict):
super(JWKSet, self).__setitem__('keys', _JWKkeys())
self.update(*args, **kwargs)
def __iter__(self):
return self['keys'].__iter__()
def __contains__(self, key):
return self['keys'].__contains__(key)
def __setitem__(self, key, val):
if key == 'keys':
self['keys'].add(val)
......@@ -639,7 +842,7 @@ class JWKSet(dict):
"""
try:
jwkset = json_decode(keyset)
except:
except Exception: # pylint: disable=broad-except
raise InvalidJWKValue()
if 'keys' not in jwkset:
......@@ -652,8 +855,6 @@ class JWKSet(dict):
else:
self[k] = v
return self
@classmethod
def from_json(cls, keyset):
"""Creates a RFC 7517 keyset from the standard JSON format.
......@@ -661,7 +862,8 @@ class JWKSet(dict):
:param keyset: The RFC 7517 representation of a JOSE Keyset.
"""
obj = cls()
return obj.import_keyset(keyset)
obj.import_keyset(keyset)
return obj
def get_key(self, kid):
"""Gets a key from the set.
......
This diff is collapsed.
......@@ -5,7 +5,7 @@ import uuid
from six import string_types
from jwcrypto.common import json_decode, json_encode
from jwcrypto.common import JWException, json_decode, json_encode
from jwcrypto.jwe import JWE
from jwcrypto.jwk import JWK, JWKSet
from jwcrypto.jws import JWS
......@@ -22,7 +22,7 @@ JWTClaimsRegistry = {'iss': 'Issuer',
'jti': 'JWT ID'}
class JWTExpired(Exception):
class JWTExpired(JWException):
"""Json Web Token is expired.
This exception is raised when a token is expired accoring to its claims.
......@@ -39,7 +39,7 @@ class JWTExpired(Exception):
super(JWTExpired, self).__init__(msg)
class JWTNotYetValid(Exception):
class JWTNotYetValid(JWException):
"""Json Web Token is not yet valid.
This exception is raised when a token is not valid yet according to its
......@@ -57,7 +57,7 @@ class JWTNotYetValid(Exception):
super(JWTNotYetValid, self).__init__(msg)
class JWTMissingClaim(Exception):
class JWTMissingClaim(JWException):
"""Json Web Token claim is invalid.
This exception is raised when a claim does not match the expected value.
......@@ -74,7 +74,7 @@ class JWTMissingClaim(Exception):
super(JWTMissingClaim, self).__init__(msg)
class JWTInvalidClaimValue(Exception):
class JWTInvalidClaimValue(JWException):
"""Json Web Token claim is invalid.
This exception is raised when a claim does not match the expected value.
......@@ -91,7 +91,7 @@ class JWTInvalidClaimValue(Exception):
super(JWTInvalidClaimValue, self).__init__(msg)
class JWTInvalidClaimFormat(Exception):
class JWTInvalidClaimFormat(JWException):
"""Json Web Token claim format is invalid.
This exception is raised when a claim is not in a valid format.
......@@ -108,7 +108,7 @@ class JWTInvalidClaimFormat(Exception):
super(JWTInvalidClaimFormat, self).__init__(msg)
class JWTMissingKeyID(Exception):
class JWTMissingKeyID(JWException):
"""Json Web Token is missing key id.
This exception is raised when trying to decode a JWT with a key set
......@@ -126,7 +126,7 @@ class JWTMissingKeyID(Exception):
super(JWTMissingKeyID, self).__init__(msg)
class JWTMissingKey(Exception):
class JWTMissingKey(JWException):
"""Json Web Token is using a key not in the key set.
This exception is raised if the key that was used is not available
......@@ -155,15 +155,15 @@ class JWT(object):
"""Creates a JWT object.
:param header: A dict or a JSON string with the JWT Header data.
:param claims: A dict or a string withthe JWT Claims data.
:param claims: A dict or a string with the JWT Claims data.
:param jwt: a 'raw' JWT token
:param key: A (:class:`jwcrypto.jwk.JWK`) key to deserialize
the token. A (:class:`jwcrypt.jwk.JWKSet`) can also be used.
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
will cause generation according to system time. Only the values
fro RFC 7519 - 4.1 are evaluated.
from RFC 7519 - 4.1 are evaluated.
:param check_claims: An optional dict of claims that must be
present in the token, if the value is not None the claim must
match exactly.
......@@ -191,15 +191,15 @@ class JWT(object):
if header:
self.header = header
if claims:
self.claims = claims
if default_claims is not None:
self._reg_claims = default_claims
if check_claims is not None:
self._check_claims = check_claims
if claims:
self.claims = claims
if jwt is not None:
self.deserialize(jwt, key)
......@@ -212,9 +212,15 @@ class JWT(object):
@header.setter
def header(self, h):
if isinstance(h, dict):
self._header = json_encode(h)
eh = json_encode(h)
else:
self._header = h
eh = h
h = json_decode(eh)
if h.get('b64') is False:
raise ValueError("b64 header is invalid."
"JWTs cannot use unencoded payloads")
self._header = eh
@property
def claims(self):
......@@ -224,6 +230,10 @@ class JWT(object):
@claims.setter
def claims(self, c):
if self._reg_claims and not isinstance(c, dict):
# decode c so we can set default claims
c = json_decode(c)
if isinstance(c, dict):
self._add_default_claims(c)
self._claims = json_encode(c)
......@@ -276,7 +286,7 @@ class JWT(object):
def _add_jti_claim(self, claims):
if 'jti' in claims or 'jti' not in self._reg_claims:
return
claims['jti'] = uuid.uuid4()
claims['jti'] = str(uuid.uuid4())
def _add_default_claims(self, claims):
if self._reg_claims is None:
......@@ -340,7 +350,7 @@ class JWT(object):
if 'exp' in claims:
self._check_exp(claims['exp'], time.time(), self._leeway)
if 'nbf' in claims:
self._check_exp(claims['nbf'], time.time(), self._leeway)
self._check_nbf(claims['nbf'], time.time(), self._leeway)
def _check_provided_claims(self):
# check_claims can be set to False to skip any check
......@@ -380,8 +390,8 @@ class JWT(object):
if value in claims[name]:
continue
raise JWTInvalidClaimValue(
"Invalid '%s' value. Expected '%s' in '%s'" % (
name, value, claims[name]))
"Invalid '%s' value. Expected '%s' to be in '%s'" % (
name, claims[name], value))
elif name == 'exp':
if value is not None:
......@@ -398,7 +408,7 @@ class JWT(object):
else:
if value is not None and value != claims[name]:
raise JWTInvalidClaimValue(
"Invalid '%s' value. Expected '%d' got '%d'" % (
"Invalid '%s' value. Expected '%s' got '%s'" % (
name, value, claims[name]))
def make_signed_token(self, key):
......@@ -437,7 +447,7 @@ class JWT(object):
:param jwt: a 'raw' JWT token.
:param key: A (:class:`jwcrypto.jwk.JWK`) verification or
decryption key, or a (:class:`jwcrypt.jwk.JWKSet`) that
decryption key, or a (:class:`jwcrypto.jwk.JWKSet`) that
contains a key indexed by the 'kid' header.
"""
c = jwt.count('.')
......
This diff is collapsed.
[bdist_wheel]
universal = 1
[aliases]
packages = clean --all egg_info bdist_wheel sdist --format=zip sdist --format=gztar
release = packages register upload
......@@ -6,7 +6,7 @@ from setuptools import setup
setup(
name = 'jwcrypto',
version = '0.3.1',
version = '0.6.0',
license = 'LGPLv3+',
maintainer = 'JWCrypto Project Contributors',
maintainer_email = 'simo@redhat.com',
......@@ -17,10 +17,14 @@ setup(
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Intended Audience :: Developers',
'Topic :: Security',
'Topic :: Software Development :: Libraries :: Python Modules'
],
data_files = [('share/doc/jwcrypto', ['LICENSE', 'README.md'])],
install_requires = [
'cryptography >= 1.5',
],
)
[tox]
envlist = lint,py27,py34,py35,pep8py2,pep8py3,doc,sphinx
envlist = lint,py27,py34,py35,py36,py37,pep8py2,pep8py3,doc,sphinx
skip_missing_interpreters = true
[testenv]
......@@ -8,17 +8,15 @@ setenv =
deps =
pytest
coverage
-r{toxinidir}/requirements.txt
sitepackages = True
commands =
{envpython} -m coverage run -m pytest --capture=no --strict {posargs}
{envpython} -bb -m coverage run -m pytest --capture=no --strict {posargs}
{envpython} -m coverage report -m
[testenv:lint]
basepython = python2.7
deps =
pylint
-r{toxinidir}/requirements.txt
sitepackages = True
commands =
{envpython} -m pylint -d c,r,i,W0613 -r n -f colorized --notes= --disable=star-args ./jwcrypto
......@@ -49,7 +47,6 @@ deps =
basepython = python2.7
commands =
doc8 --allow-long-titles README.md
python setup.py check --restructuredtext --metadata --strict
markdown_py README.md -f {toxworkdir}/README.md.html
[testenv:sphinx]
......@@ -57,7 +54,6 @@ basepython = python2.7
changedir = docs/source
deps =
sphinx < 1.3.0
-r{toxinidir}/requirements.txt
commands =
sphinx-build -v -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
......