...
 
Commits (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.
This diff is collapsed.
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
......