Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for RSASSA-PSS algorithms (PS256, PS384, PS512) #132

Merged
merged 2 commits into from
Apr 10, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
[Unreleased][unreleased]
-------------------------------------------------------------------------
### Changed
- Added flexible and complete verification options during decode #131
- Added support for PS256, PS384, and PS512 algorithms. #132
- Added this CHANGELOG.md file
- Added flexible and complete verification options. #131


### Fixed
- Placeholder
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ except jwt.InvalidTokenError:
```

You may also override exception checking via an `options` dictionary. The default
options are as follows:
options are as follows:

```python
options = {
Expand Down Expand Up @@ -112,6 +112,9 @@ currently supports:
* RS256 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-256 hash algorithm
* RS384 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash algorithm
* RS512 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash algorithm
* PS256 - RSASSA-PSS signature using SHA-256 and MGF1 padding with SHA-256
* PS384 - RSASSA-PSS signature using SHA-384 and MGF1 padding with SHA-384
* PS512 - RSASSA-PSS signature using SHA-512 and MGF1 padding with SHA-512

### Encoding
You can specify which algorithm you would like to use to sign the JWT
Expand Down
52 changes: 45 additions & 7 deletions jwt/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ def get_default_algorithms():
'RS512': RSAAlgorithm(RSAAlgorithm.SHA512),
'ES256': ECAlgorithm(ECAlgorithm.SHA256),
'ES384': ECAlgorithm(ECAlgorithm.SHA384),
'ES512': ECAlgorithm(ECAlgorithm.SHA512)
'ES512': ECAlgorithm(ECAlgorithm.SHA512),
'PS256': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256),
'PS384': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384),
'PS512': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512)
})

return default_algorithms
Expand Down Expand Up @@ -145,7 +148,7 @@ class RSAAlgorithm(Algorithm):
SHA512 = hashes.SHA512

def __init__(self, hash_alg):
self.hash_alg = hash_alg()
self.hash_alg = hash_alg

def prepare_key(self, key):
if isinstance(key, RSAPrivateKey) or \
Expand All @@ -171,7 +174,7 @@ def prepare_key(self, key):
def sign(self, msg, key):
signer = key.signer(
padding.PKCS1v15(),
self.hash_alg
self.hash_alg()
)

signer.update(msg)
Expand All @@ -181,7 +184,7 @@ def verify(self, msg, key, sig):
verifier = key.verifier(
sig,
padding.PKCS1v15(),
self.hash_alg
self.hash_alg()
)

verifier.update(msg)
Expand All @@ -202,7 +205,7 @@ class ECAlgorithm(Algorithm):
SHA512 = hashes.SHA512

def __init__(self, hash_alg):
self.hash_alg = hash_alg()
self.hash_alg = hash_alg

def prepare_key(self, key):
if isinstance(key, EllipticCurvePrivateKey) or \
Expand All @@ -227,13 +230,48 @@ def prepare_key(self, key):
return key

def sign(self, msg, key):
signer = key.signer(ec.ECDSA(self.hash_alg))
signer = key.signer(ec.ECDSA(self.hash_alg()))

signer.update(msg)
return signer.finalize()

def verify(self, msg, key, sig):
verifier = key.verifier(sig, ec.ECDSA(self.hash_alg))
verifier = key.verifier(sig, ec.ECDSA(self.hash_alg()))

verifier.update(msg)

try:
verifier.verify()
return True
except InvalidSignature:
return False

class RSAPSSAlgorithm(RSAAlgorithm):
"""
Performs a signature using RSASSA-PSS with MGF1
"""

def sign(self, msg, key):
signer = key.signer(
padding.PSS(
mgf=padding.MGF1(self.hash_alg()),
salt_length=padding.PSS.MAX_LENGTH
),
self.hash_alg()
)

signer.update(msg)
return signer.finalize()

def verify(self, msg, key, sig):
verifier = key.verifier(
sig,
padding.PSS(
mgf=padding.MGF1(self.hash_alg()),
salt_length=padding.PSS.MAX_LENGTH
),
self.hash_alg()
)

verifier.update(msg)

Expand Down
72 changes: 65 additions & 7 deletions tests/test_algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .utils import ensure_bytes, ensure_unicode, key_path

try:
from jwt.algorithms import RSAAlgorithm, ECAlgorithm
from jwt.algorithms import RSAAlgorithm, ECAlgorithm, RSAPSSAlgorithm

has_crypto = True
except ImportError:
Expand Down Expand Up @@ -169,34 +169,92 @@ def test_ec_should_accept_unicode_key(self):
def test_ec_verify_should_return_false_if_signature_invalid(self):
algo = ECAlgorithm(ECAlgorithm.SHA256)

jwt_message = ensure_bytes('Hello World!')
message = ensure_bytes('Hello World!')

# Mess up the signature by replacing a known byte
jwt_sig = base64.b64decode(ensure_bytes(
sig = base64.b64decode(ensure_bytes(
'MIGIAkIB9vYz+inBL8aOTA4auYz/zVuig7TT1bQgKROIQX9YpViHkFa4DT5'
'5FuFKn9XzVlk90p6ldEj42DC9YecXHbC2t+cCQgCicY+8f3f/KCNtWK7cif'
'6vdsVwm6Lrjs0Ag6ZqCf+olN11hVt1qKBC4lXppqB1gNWEmNQaiz1z2QRyc'
'zJ8hSJmbw=='.replace('r', 's')))

with open(key_path('testkey_ec.pub'), 'r') as keyfile:
jwt_pub_key = algo.prepare_key(keyfile.read())
pub_key = algo.prepare_key(keyfile.read())

result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
result = algo.verify(message, pub_key, sig)
self.assertFalse(result)

@unittest.skipIf(not has_crypto, 'Not supported without cryptography library')
def test_ec_verify_should_return_true_if_signature_valid(self):
algo = ECAlgorithm(ECAlgorithm.SHA256)

jwt_message = ensure_bytes('Hello World!')
message = ensure_bytes('Hello World!')

jwt_sig = base64.b64decode(ensure_bytes(
sig = base64.b64decode(ensure_bytes(
'MIGIAkIB9vYz+inBL8aOTA4auYz/zVuig7TT1bQgKROIQX9YpViHkFa4DT5'
'5FuFKn9XzVlk90p6ldEj42DC9YecXHbC2t+cCQgCicY+8f3f/KCNtWK7cif'
'6vdsVwm6Lrjs0Ag6ZqCf+olN11hVt1qKBC4lXppqB1gNWEmNQaiz1z2QRyc'
'zJ8hSJmbw=='))

with open(key_path('testkey_ec.pub'), 'r') as keyfile:
pub_key = algo.prepare_key(keyfile.read())

result = algo.verify(message, pub_key, sig)
self.assertTrue(result)

@unittest.skipIf(not has_crypto, 'Not supported without cryptography library')
def test_rsa_pss_sign_then_verify_should_return_true(self):
algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256)

message = ensure_bytes('Hello World!')

with open(key_path('testkey_rsa'), 'r') as keyfile:
priv_key = algo.prepare_key(keyfile.read())
sig = algo.sign(message, priv_key)

with open(key_path('testkey_rsa.pub'), 'r') as keyfile:
pub_key = algo.prepare_key(keyfile.read())

result = algo.verify(message, pub_key, sig)
self.assertTrue(result)

@unittest.skipIf(not has_crypto, 'Not supported without cryptography library')
def test_rsa_pss_verify_should_return_false_if_signature_invalid(self):
algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256)

jwt_message = ensure_bytes('Hello World!')

jwt_sig = base64.b64decode(ensure_bytes(
'ywKAUGRIDC//6X+tjvZA96yEtMqpOrSppCNfYI7NKyon3P7doud5v65oWNu'
'vQsz0fzPGfF7mQFGo9Cm9Vn0nljm4G6PtqZRbz5fXNQBH9k10gq34AtM02c'
'/cveqACQ8gF3zxWh6qr9jVqIpeMEaEBIkvqG954E0HT9s9ybHShgHX9mlWk'
'186/LopP4xe5c/hxOQjwhv6yDlTiwJFiqjNCvj0GyBKsc4iECLGIIO+4mC4'
'daOCWqbpZDuLb1imKpmm8Nsm56kAxijMLZnpCcnPgyb7CqG+B93W9GHglA5'
'drUeR1gRtO7vqbZMsCAQ4bpjXxwbYyjQlEVuMl73UL6sOWg=='))

jwt_sig += ensure_bytes('123') # Signature is now invalid

with open(key_path('testkey_rsa.pub'), 'r') as keyfile:
jwt_pub_key = algo.prepare_key(keyfile.read())

result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
self.assertFalse(result)

@unittest.skipIf(not has_crypto, 'Not supported without cryptography library')
def test_rsa_pss_verify_should_return_true_if_signature_valid(self):
algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256)

jwt_message = ensure_bytes('Hello World!')

jwt_sig = base64.b64decode(ensure_bytes(
'ywKAUGRIDC//6X+tjvZA96yEtMqpOrSppCNfYI7NKyon3P7doud5v65oWNu'
'vQsz0fzPGfF7mQFGo9Cm9Vn0nljm4G6PtqZRbz5fXNQBH9k10gq34AtM02c'
'/cveqACQ8gF3zxWh6qr9jVqIpeMEaEBIkvqG954E0HT9s9ybHShgHX9mlWk'
'186/LopP4xe5c/hxOQjwhv6yDlTiwJFiqjNCvj0GyBKsc4iECLGIIO+4mC4'
'daOCWqbpZDuLb1imKpmm8Nsm56kAxijMLZnpCcnPgyb7CqG+B93W9GHglA5'
'drUeR1gRtO7vqbZMsCAQ4bpjXxwbYyjQlEVuMl73UL6sOWg=='))

with open(key_path('testkey_rsa.pub'), 'r') as keyfile:
jwt_pub_key = algo.prepare_key(keyfile.read())

result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
Expand Down
7 changes: 7 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,10 +615,17 @@ def test_rsa_related_algorithms(self):
self.assertTrue('RS256' in jwt_algorithms)
self.assertTrue('RS384' in jwt_algorithms)
self.assertTrue('RS512' in jwt_algorithms)
self.assertTrue('PS256' in jwt_algorithms)
self.assertTrue('PS384' in jwt_algorithms)
self.assertTrue('PS512' in jwt_algorithms)

else:
self.assertFalse('RS256' in jwt_algorithms)
self.assertFalse('RS384' in jwt_algorithms)
self.assertFalse('RS512' in jwt_algorithms)
self.assertFalse('PS256' in jwt_algorithms)
self.assertFalse('PS384' in jwt_algorithms)
self.assertFalse('PS512' in jwt_algorithms)

@unittest.skipIf(not has_crypto, "Can't run without cryptography library")
def test_encode_decode_with_ecdsa_sha256(self):
Expand Down