...
 
Commits (64)
...@@ -12,15 +12,17 @@ matrix: ...@@ -12,15 +12,17 @@ matrix:
env: TOXENV=py34 env: TOXENV=py34
- python: 3.5 - python: 3.5
env: TOXENV=py35 env: TOXENV=py35
- python: 3.5 - python: 3.6
env: TOXENV=py36
- python: 3.6
env: TOXENV=doc env: TOXENV=doc
- python: 3.5 - python: 3.6
env: TOXENV=sphinx env: TOXENV=sphinx
- python: 3.5 - python: 3.6
env: TOXENV=lint env: TOXENV=lint
- python: 2.7 - python: 2.7
env: TOXENV=pep8py2 env: TOXENV=pep8py2
- python: 3.5 - python: 3.6
env: TOXENV=pep8py3 env: TOXENV=pep8py3
install: install:
......
include LICENSE README.md include LICENSE README.md
include tox.ini setup.cfg
...@@ -17,11 +17,19 @@ clean: ...@@ -17,11 +17,19 @@ clean:
cscope: cscope:
git ls-files | xargs pycscope 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: test:
rm -f .coverage rm -f .coverage
tox -e py27 tox -e py27
tox -e py34 --skip-missing-interpreter tox -e py34 --skip-missing-interpreter
tox -e py35 --skip-missing-interpreter tox -e py35 --skip-missing-interpreter
tox -e py36 --skip-missing-interpreter
tox -e py37 --skip-missing-interpreter
DOCS_DIR = docs DOCS_DIR = docs
.PHONY: docs .PHONY: docs
......
[![Build Status](https://travis-ci.org/latchset/jwcrypto.svg?branch=master)](https://travis-ci.org/latchset/jwcrypto)
JWCrypto JWCrypto
======== ========
......
...@@ -46,16 +46,16 @@ master_doc = 'index' ...@@ -46,16 +46,16 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'JWCrypto' 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 # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.3' version = '0.6'
# The full version, including alpha/beta/rc tags. # 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 # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
......
...@@ -47,3 +47,52 @@ Registries ...@@ -47,3 +47,52 @@ Registries
.. autodata:: jwcrypto.jwe.JWEHeaderRegistry .. autodata:: jwcrypto.jwe.JWEHeaderRegistry
:annotation: :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:: ...@@ -85,3 +85,6 @@ Import a P-256 Public Key::
"crv":"P-256","kty":"EC"} "crv":"P-256","kty":"EC"}
>>> key = jwk.JWK(**expkey) >>> 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 ...@@ -43,3 +43,23 @@ Registries
.. autodata:: jwcrypto.jws.JWSHeaderRegistry .. autodata:: jwcrypto.jws.JWSHeaderRegistry
:annotation: :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): ...@@ -16,12 +16,12 @@ def base64url_encode(payload):
def base64url_decode(payload): def base64url_decode(payload):
l = len(payload) % 4 size = len(payload) % 4
if l == 2: if size == 2:
payload += '==' payload += '=='
elif l == 3: elif size == 3:
payload += '=' payload += '='
elif l != 0: elif size != 0:
raise ValueError('Invalid base64 string') raise ValueError('Invalid base64 string')
return urlsafe_b64decode(payload.encode('utf-8')) return urlsafe_b64decode(payload.encode('utf-8'))
...@@ -40,9 +40,67 @@ def json_decode(string): ...@@ -40,9 +40,67 @@ def json_decode(string):
return json.loads(string) return json.loads(string)
class InvalidJWAAlgorithm(Exception): class JWException(Exception):
pass
class InvalidJWAAlgorithm(JWException):
def __init__(self, message=None): def __init__(self, message=None):
msg = 'Invalid JWS Algorithm name' msg = 'Invalid JWA Algorithm name'
if message: if message:
msg += ' (%s)' % message msg += ' (%s)' % message
super(InvalidJWAAlgorithm, self).__init__(msg) 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.
This diff is collapsed.
This diff is collapsed.
...@@ -5,7 +5,7 @@ import uuid ...@@ -5,7 +5,7 @@ import uuid
from six import string_types 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.jwe import JWE
from jwcrypto.jwk import JWK, JWKSet from jwcrypto.jwk import JWK, JWKSet
from jwcrypto.jws import JWS from jwcrypto.jws import JWS
...@@ -22,7 +22,7 @@ JWTClaimsRegistry = {'iss': 'Issuer', ...@@ -22,7 +22,7 @@ JWTClaimsRegistry = {'iss': 'Issuer',
'jti': 'JWT ID'} 'jti': 'JWT ID'}
class JWTExpired(Exception): class JWTExpired(JWException):
"""Json Web Token is expired. """Json Web Token is expired.
This exception is raised when a token is expired accoring to its claims. This exception is raised when a token is expired accoring to its claims.
...@@ -39,7 +39,7 @@ class JWTExpired(Exception): ...@@ -39,7 +39,7 @@ class JWTExpired(Exception):
super(JWTExpired, self).__init__(msg) super(JWTExpired, self).__init__(msg)
class JWTNotYetValid(Exception): class JWTNotYetValid(JWException):
"""Json Web Token is not yet valid. """Json Web Token is not yet valid.
This exception is raised when a token is not valid yet according to its This exception is raised when a token is not valid yet according to its
...@@ -57,7 +57,7 @@ class JWTNotYetValid(Exception): ...@@ -57,7 +57,7 @@ class JWTNotYetValid(Exception):
super(JWTNotYetValid, self).__init__(msg) super(JWTNotYetValid, self).__init__(msg)
class JWTMissingClaim(Exception): class JWTMissingClaim(JWException):
"""Json Web Token claim is invalid. """Json Web Token claim is invalid.
This exception is raised when a claim does not match the expected value. This exception is raised when a claim does not match the expected value.
...@@ -74,7 +74,7 @@ class JWTMissingClaim(Exception): ...@@ -74,7 +74,7 @@ class JWTMissingClaim(Exception):
super(JWTMissingClaim, self).__init__(msg) super(JWTMissingClaim, self).__init__(msg)
class JWTInvalidClaimValue(Exception): class JWTInvalidClaimValue(JWException):
"""Json Web Token claim is invalid. """Json Web Token claim is invalid.
This exception is raised when a claim does not match the expected value. This exception is raised when a claim does not match the expected value.
...@@ -91,7 +91,7 @@ class JWTInvalidClaimValue(Exception): ...@@ -91,7 +91,7 @@ class JWTInvalidClaimValue(Exception):
super(JWTInvalidClaimValue, self).__init__(msg) super(JWTInvalidClaimValue, self).__init__(msg)
class JWTInvalidClaimFormat(Exception): class JWTInvalidClaimFormat(JWException):
"""Json Web Token claim format is invalid. """Json Web Token claim format is invalid.
This exception is raised when a claim is not in a valid format. This exception is raised when a claim is not in a valid format.
...@@ -108,7 +108,7 @@ class JWTInvalidClaimFormat(Exception): ...@@ -108,7 +108,7 @@ class JWTInvalidClaimFormat(Exception):
super(JWTInvalidClaimFormat, self).__init__(msg) super(JWTInvalidClaimFormat, self).__init__(msg)
class JWTMissingKeyID(Exception): class JWTMissingKeyID(JWException):
"""Json Web Token is missing key id. """Json Web Token is missing key id.
This exception is raised when trying to decode a JWT with a key set This exception is raised when trying to decode a JWT with a key set
...@@ -126,7 +126,7 @@ class JWTMissingKeyID(Exception): ...@@ -126,7 +126,7 @@ class JWTMissingKeyID(Exception):
super(JWTMissingKeyID, self).__init__(msg) super(JWTMissingKeyID, self).__init__(msg)
class JWTMissingKey(Exception): class JWTMissingKey(JWException):
"""Json Web Token is using a key not in the key set. """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 This exception is raised if the key that was used is not available
...@@ -155,15 +155,15 @@ class JWT(object): ...@@ -155,15 +155,15 @@ class JWT(object):
"""Creates a JWT object. """Creates a JWT object.
:param header: A dict or a JSON string with the JWT Header data. :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 jwt: a 'raw' JWT token
:param key: A (:class:`jwcrypto.jwk.JWK`) key to deserialize :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 algs: An optional list of allowed algorithms
:param default_claims: An optional dict with default values for :param default_claims: An optional dict with default values for
registred claims. A None value for NumericDate type claims registred claims. A None value for NumericDate type claims
will cause generation according to system time. Only the values 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 :param check_claims: An optional dict of claims that must be
present in the token, if the value is not None the claim must present in the token, if the value is not None the claim must
match exactly. match exactly.
...@@ -191,15 +191,15 @@ class JWT(object): ...@@ -191,15 +191,15 @@ class JWT(object):
if header: if header:
self.header = header self.header = header
if claims:
self.claims = claims
if default_claims is not None: if default_claims is not None:
self._reg_claims = default_claims self._reg_claims = default_claims
if check_claims is not None: if check_claims is not None:
self._check_claims = check_claims self._check_claims = check_claims
if claims:
self.claims = claims
if jwt is not None: if jwt is not None:
self.deserialize(jwt, key) self.deserialize(jwt, key)
...@@ -212,9 +212,15 @@ class JWT(object): ...@@ -212,9 +212,15 @@ class JWT(object):
@header.setter @header.setter
def header(self, h): def header(self, h):
if isinstance(h, dict): if isinstance(h, dict):
self._header = json_encode(h) eh = json_encode(h)
else: 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 @property
def claims(self): def claims(self):
...@@ -224,6 +230,10 @@ class JWT(object): ...@@ -224,6 +230,10 @@ class JWT(object):
@claims.setter @claims.setter
def claims(self, c): 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): if isinstance(c, dict):
self._add_default_claims(c) self._add_default_claims(c)
self._claims = json_encode(c) self._claims = json_encode(c)
...@@ -276,7 +286,7 @@ class JWT(object): ...@@ -276,7 +286,7 @@ class JWT(object):
def _add_jti_claim(self, claims): def _add_jti_claim(self, claims):
if 'jti' in claims or 'jti' not in self._reg_claims: if 'jti' in claims or 'jti' not in self._reg_claims:
return return
claims['jti'] = uuid.uuid4() claims['jti'] = str(uuid.uuid4())
def _add_default_claims(self, claims): def _add_default_claims(self, claims):
if self._reg_claims is None: if self._reg_claims is None:
...@@ -340,7 +350,7 @@ class JWT(object): ...@@ -340,7 +350,7 @@ class JWT(object):
if 'exp' in claims: if 'exp' in claims:
self._check_exp(claims['exp'], time.time(), self._leeway) self._check_exp(claims['exp'], time.time(), self._leeway)
if 'nbf' in claims: 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): def _check_provided_claims(self):
# check_claims can be set to False to skip any check # check_claims can be set to False to skip any check
...@@ -380,8 +390,8 @@ class JWT(object): ...@@ -380,8 +390,8 @@ class JWT(object):
if value in claims[name]: if value in claims[name]:
continue continue
raise JWTInvalidClaimValue( raise JWTInvalidClaimValue(
"Invalid '%s' value. Expected '%s' in '%s'" % ( "Invalid '%s' value. Expected '%s' to be in '%s'" % (
name, value, claims[name])) name, claims[name], value))
elif name == 'exp': elif name == 'exp':
if value is not None: if value is not None:
...@@ -398,7 +408,7 @@ class JWT(object): ...@@ -398,7 +408,7 @@ class JWT(object):
else: else:
if value is not None and value != claims[name]: if value is not None and value != claims[name]:
raise JWTInvalidClaimValue( raise JWTInvalidClaimValue(
"Invalid '%s' value. Expected '%d' got '%d'" % ( "Invalid '%s' value. Expected '%s' got '%s'" % (
name, value, claims[name])) name, value, claims[name]))
def make_signed_token(self, key): def make_signed_token(self, key):
...@@ -437,7 +447,7 @@ class JWT(object): ...@@ -437,7 +447,7 @@ class JWT(object):
:param jwt: a 'raw' JWT token. :param jwt: a 'raw' JWT token.
:param key: A (:class:`jwcrypto.jwk.JWK`) verification or :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. contains a key indexed by the 'kid' header.
""" """
c = jwt.count('.') 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 ...@@ -6,7 +6,7 @@ from setuptools import setup
setup( setup(
name = 'jwcrypto', name = 'jwcrypto',
version = '0.3.1', version = '0.6.0',
license = 'LGPLv3+', license = 'LGPLv3+',
maintainer = 'JWCrypto Project Contributors', maintainer = 'JWCrypto Project Contributors',
maintainer_email = 'simo@redhat.com', maintainer_email = 'simo@redhat.com',
...@@ -17,10 +17,14 @@ setup( ...@@ -17,10 +17,14 @@ setup(
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Topic :: Security', 'Topic :: Security',
'Topic :: Software Development :: Libraries :: Python Modules' 'Topic :: Software Development :: Libraries :: Python Modules'
], ],
data_files = [('share/doc/jwcrypto', ['LICENSE', 'README.md'])], data_files = [('share/doc/jwcrypto', ['LICENSE', 'README.md'])],
install_requires = [
'cryptography >= 1.5',
],
) )
[tox] [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 skip_missing_interpreters = true
[testenv] [testenv]
...@@ -8,17 +8,15 @@ setenv = ...@@ -8,17 +8,15 @@ setenv =
deps = deps =
pytest pytest
coverage coverage
-r{toxinidir}/requirements.txt
sitepackages = True sitepackages = True
commands = 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 {envpython} -m coverage report -m
[testenv:lint] [testenv:lint]
basepython = python2.7 basepython = python2.7
deps = deps =
pylint pylint
-r{toxinidir}/requirements.txt
sitepackages = True sitepackages = True
commands = commands =
{envpython} -m pylint -d c,r,i,W0613 -r n -f colorized --notes= --disable=star-args ./jwcrypto {envpython} -m pylint -d c,r,i,W0613 -r n -f colorized --notes= --disable=star-args ./jwcrypto
...@@ -49,7 +47,6 @@ deps = ...@@ -49,7 +47,6 @@ deps =
basepython = python2.7 basepython = python2.7
commands = commands =
doc8 --allow-long-titles README.md doc8 --allow-long-titles README.md
python setup.py check --restructuredtext --metadata --strict
markdown_py README.md -f {toxworkdir}/README.md.html markdown_py README.md -f {toxworkdir}/README.md.html
[testenv:sphinx] [testenv:sphinx]
...@@ -57,7 +54,6 @@ basepython = python2.7 ...@@ -57,7 +54,6 @@ basepython = python2.7
changedir = docs/source changedir = docs/source
deps = deps =
sphinx < 1.3.0 sphinx < 1.3.0
-r{toxinidir}/requirements.txt
commands = commands =
sphinx-build -v -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html sphinx-build -v -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
......