Skip to content

Commit b83d6e1

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 62c0897 + 5caa1af commit b83d6e1

18 files changed

+630
-189
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Change Log
44
All notable changes to this project will be documented in this file.
55
This project adheres to [Semantic Versioning](http://semver.org/).
66

7+
[Unreleased][unreleased]
8+
-------------------------------------------------------------------------
9+
### Changed
10+
- Renamed commandline script `jwt` to `jwt-cli` to avoid issues with the script clobbering the `jwt` module in some circumstances.
11+
12+
### Fixed
13+
714
[v1.4.2][1.4.2]
815
-------------------------------------------------------------------------
916
### Fixed
@@ -89,7 +96,7 @@ rarely used. Users affected by this should upgrade to 3.3+.
8996
- Fixed a security vulnerability by adding support for a whitelist of allowed `alg` values `jwt.decode(algorithms=[])`. [#110][110]
9097

9198

92-
[unreleased]: https://github.com/jpadilla/pyjwt/compare/1.3.0...HEAD
99+
[unreleased]: https://github.com/jpadilla/pyjwt/compare/1.4.2...HEAD
93100
[1.0.0]: https://github.com/jpadilla/pyjwt/compare/0.4.3...1.0.0
94101
[1.0.1]: https://github.com/jpadilla/pyjwt/compare/1.0.0...1.0.1
95102
[1.0.1]: https://github.com/jpadilla/pyjwt/compare/1.0.0...1.0.1

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
include README.md
1+
include README.rst
22
include CHANGELOG.md
33
include LICENSE
44
include AUTHORS

README.md

Lines changed: 0 additions & 48 deletions
This file was deleted.

README.rst

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
PyJWT
2+
=====
3+
4+
.. image:: https://secure.travis-ci.org/jpadilla/pyjwt.svg?branch=master
5+
:target: http://travis-ci.org/jpadilla/pyjwt?branch=master
6+
7+
.. image:: https://ci.appveyor.com/api/projects/status/h8nt70aqtwhht39t?svg=true
8+
:target: https://ci.appveyor.com/project/jpadilla/pyjwt
9+
10+
.. image:: https://img.shields.io/pypi/v/pyjwt.svg
11+
:target: https://pypi.python.org/pypi/pyjwt
12+
13+
.. image:: https://coveralls.io/repos/jpadilla/pyjwt/badge.svg?branch=master
14+
:target: https://coveralls.io/r/jpadilla/pyjwt?branch=master
15+
16+
.. image:: https://readthedocs.org/projects/pyjwt/badge/?version=latest
17+
:target: https://pyjwt.readthedocs.io
18+
19+
A Python implementation of `RFC
20+
7519 <https://tools.ietf.org/html/rfc7519>`_. Original implementation
21+
was written by `@progrium <https://github.com/progrium>`_.
22+
23+
Installing
24+
----------
25+
26+
Install with **pip**:
27+
28+
.. code-block:: sh
29+
30+
$ pip install PyJWT
31+
32+
Usage
33+
-----
34+
35+
.. code:: python
36+
37+
>>> import jwt
38+
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
39+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
40+
41+
>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
42+
{'some': 'payload'}
43+
44+
Documentation
45+
-------------
46+
47+
View the full docs online at https://pyjwt.readthedocs.io/en/latest/
48+
49+
Tests
50+
-----
51+
52+
You can run tests from the project root after cloning with:
53+
54+
.. code-block:: sh
55+
56+
$ python setup.py test

docs/algorithms.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This library currently supports:
99
* HS512 - HMAC using SHA-512 hash algorithm
1010
* ES256 - ECDSA signature algorithm using SHA-256 hash algorithm
1111
* ES384 - ECDSA signature algorithm using SHA-384 hash algorithm
12-
* ES512 - ECDSA signature algorithm using SHA-512 hash algorithm
12+
* ES521 - ECDSA signature algorithm using SHA-512 hash algorithm
1313
* RS256 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-256 hash algorithm
1414
* RS384 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash algorithm
1515
* RS512 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash algorithm

jwt/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def main():
3131
'''
3232
p = optparse.OptionParser(
3333
usage=usage,
34-
prog=__package__,
34+
prog='pyjwt',
3535
version='%s %s' % (__package__, __version__),
3636
)
3737

jwt/algorithms.py

Lines changed: 146 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import hashlib
22
import hmac
3+
import json
34

4-
from .compat import binary_type, constant_time_compare, is_string_type
5+
6+
from .compat import constant_time_compare, string_types
57
from .exceptions import InvalidKeyError
6-
from .utils import der_to_raw_signature, raw_to_der_signature
8+
from .utils import (
9+
base64url_decode, base64url_encode, der_to_raw_signature,
10+
force_bytes, force_unicode, from_base64url_uint, raw_to_der_signature,
11+
to_base64url_uint
12+
)
713

814
try:
915
from cryptography.hazmat.primitives import hashes
1016
from cryptography.hazmat.primitives.serialization import (
1117
load_pem_private_key, load_pem_public_key, load_ssh_public_key
1218
)
1319
from cryptography.hazmat.primitives.asymmetric.rsa import (
14-
RSAPrivateKey, RSAPublicKey
20+
RSAPrivateKey, RSAPublicKey, RSAPrivateNumbers, RSAPublicNumbers,
21+
rsa_recover_prime_factors, rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp
1522
)
1623
from cryptography.hazmat.primitives.asymmetric.ec import (
1724
EllipticCurvePrivateKey, EllipticCurvePublicKey
@@ -32,6 +39,7 @@ def _get_crypto_algorithms():
3239
'RS512': None,
3340
'ES256': None,
3441
'ES384': None,
42+
'ES521': None,
3543
'ES512': None,
3644
'PS256': None,
3745
'PS384': None,
@@ -44,6 +52,7 @@ def _get_crypto_algorithms():
4452
crypto_algorithms['RS512'] = RSAAlgorithm(RSAAlgorithm.SHA512)
4553
crypto_algorithms['ES256'] = ECAlgorithm(ECAlgorithm.SHA256)
4654
crypto_algorithms['ES384'] = ECAlgorithm(ECAlgorithm.SHA384)
55+
crypto_algorithms['ES521'] = ECAlgorithm(ECAlgorithm.SHA512),
4756
crypto_algorithms['ES512'] = ECAlgorithm(ECAlgorithm.SHA512)
4857
crypto_algorithms['PS256'] = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256)
4958
crypto_algorithms['PS384'] = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384)
@@ -106,6 +115,20 @@ def verify(self, msg, key, sig):
106115
"""
107116
raise NotImplementedError
108117

118+
@staticmethod
119+
def to_jwk(key_obj):
120+
"""
121+
Serializes a given RSA key into a JWK
122+
"""
123+
raise NotImplementedError
124+
125+
@staticmethod
126+
def from_jwk(jwk):
127+
"""
128+
Deserializes a given RSA key from JWK back into a PublicKey or PrivateKey object
129+
"""
130+
raise NotImplementedError
131+
109132

110133
class NoneAlgorithm(Algorithm):
111134
"""
@@ -141,11 +164,7 @@ def __init__(self, hash_alg):
141164
self.hash_alg = hash_alg
142165

143166
def prepare_key(self, key):
144-
if not is_string_type(key):
145-
raise TypeError('Expecting a string- or bytes-formatted key.')
146-
147-
if not isinstance(key, binary_type):
148-
key = key.encode('utf-8')
167+
key = force_bytes(key)
149168

150169
invalid_strings = [
151170
b'-----BEGIN PUBLIC KEY-----',
@@ -160,6 +179,22 @@ def prepare_key(self, key):
160179

161180
return key
162181

182+
@staticmethod
183+
def to_jwk(key_obj):
184+
return json.dumps({
185+
'k': force_unicode(base64url_encode(force_bytes(key_obj))),
186+
'kty': 'oct'
187+
})
188+
189+
@staticmethod
190+
def from_jwk(jwk):
191+
obj = json.loads(jwk)
192+
193+
if obj.get('kty') != 'oct':
194+
raise InvalidKeyError('Not an HMAC key')
195+
196+
return base64url_decode(obj['k'])
197+
163198
def sign(self, msg, key):
164199
return hmac.new(key, msg, self.hash_alg).digest()
165200

@@ -185,9 +220,8 @@ def prepare_key(self, key):
185220
isinstance(key, RSAPublicKey):
186221
return key
187222

188-
if is_string_type(key):
189-
if not isinstance(key, binary_type):
190-
key = key.encode('utf-8')
223+
if isinstance(key, string_types):
224+
key = force_bytes(key)
191225

192226
try:
193227
if key.startswith(b'ssh-rsa'):
@@ -201,6 +235,105 @@ def prepare_key(self, key):
201235

202236
return key
203237

238+
@staticmethod
239+
def to_jwk(key_obj):
240+
obj = None
241+
242+
if getattr(key_obj, 'private_numbers', None):
243+
# Private key
244+
numbers = key_obj.private_numbers()
245+
246+
obj = {
247+
'kty': 'RSA',
248+
'key_ops': ['sign'],
249+
'n': force_unicode(to_base64url_uint(numbers.public_numbers.n)),
250+
'e': force_unicode(to_base64url_uint(numbers.public_numbers.e)),
251+
'd': force_unicode(to_base64url_uint(numbers.d)),
252+
'p': force_unicode(to_base64url_uint(numbers.p)),
253+
'q': force_unicode(to_base64url_uint(numbers.q)),
254+
'dp': force_unicode(to_base64url_uint(numbers.dmp1)),
255+
'dq': force_unicode(to_base64url_uint(numbers.dmq1)),
256+
'qi': force_unicode(to_base64url_uint(numbers.iqmp))
257+
}
258+
259+
elif getattr(key_obj, 'verifier', None):
260+
# Public key
261+
numbers = key_obj.public_numbers()
262+
263+
obj = {
264+
'kty': 'RSA',
265+
'key_ops': ['verify'],
266+
'n': force_unicode(to_base64url_uint(numbers.n)),
267+
'e': force_unicode(to_base64url_uint(numbers.e))
268+
}
269+
else:
270+
raise InvalidKeyError('Not a public or private key')
271+
272+
return json.dumps(obj)
273+
274+
@staticmethod
275+
def from_jwk(jwk):
276+
try:
277+
obj = json.loads(jwk)
278+
except ValueError:
279+
raise InvalidKeyError('Key is not valid JSON')
280+
281+
if obj.get('kty') != 'RSA':
282+
raise InvalidKeyError('Not an RSA key')
283+
284+
if 'd' in obj and 'e' in obj and 'n' in obj:
285+
# Private key
286+
if 'oth' in obj:
287+
raise InvalidKeyError('Unsupported RSA private key: > 2 primes not supported')
288+
289+
other_props = ['p', 'q', 'dp', 'dq', 'qi']
290+
props_found = [prop in obj for prop in other_props]
291+
any_props_found = any(props_found)
292+
293+
if any_props_found and not all(props_found):
294+
raise InvalidKeyError('RSA key must include all parameters if any are present besides d')
295+
296+
public_numbers = RSAPublicNumbers(
297+
from_base64url_uint(obj['e']), from_base64url_uint(obj['n'])
298+
)
299+
300+
if any_props_found:
301+
numbers = RSAPrivateNumbers(
302+
d=from_base64url_uint(obj['d']),
303+
p=from_base64url_uint(obj['p']),
304+
q=from_base64url_uint(obj['q']),
305+
dmp1=from_base64url_uint(obj['dp']),
306+
dmq1=from_base64url_uint(obj['dq']),
307+
iqmp=from_base64url_uint(obj['qi']),
308+
public_numbers=public_numbers
309+
)
310+
else:
311+
d = from_base64url_uint(obj['d'])
312+
p, q = rsa_recover_prime_factors(
313+
public_numbers.n, d, public_numbers.e
314+
)
315+
316+
numbers = RSAPrivateNumbers(
317+
d=d,
318+
p=p,
319+
q=q,
320+
dmp1=rsa_crt_dmp1(d, p),
321+
dmq1=rsa_crt_dmq1(d, q),
322+
iqmp=rsa_crt_iqmp(p, q),
323+
public_numbers=public_numbers
324+
)
325+
326+
return numbers.private_key(default_backend())
327+
elif 'n' in obj and 'e' in obj:
328+
# Public key
329+
numbers = RSAPublicNumbers(
330+
from_base64url_uint(obj['e']), from_base64url_uint(obj['n'])
331+
)
332+
333+
return numbers.public_key(default_backend())
334+
else:
335+
raise InvalidKeyError('Not a public or private key')
336+
204337
def sign(self, msg, key):
205338
signer = key.signer(
206339
padding.PKCS1v15(),
@@ -242,9 +375,8 @@ def prepare_key(self, key):
242375
isinstance(key, EllipticCurvePublicKey):
243376
return key
244377

245-
if is_string_type(key):
246-
if not isinstance(key, binary_type):
247-
key = key.encode('utf-8')
378+
if isinstance(key, string_types):
379+
key = force_bytes(key)
248380

249381
# Attempt to load key. We don't know if it's
250382
# a Signing Key or a Verifying Key, so we try

0 commit comments

Comments
 (0)