Skip to content

Commit ebcbed4

Browse files
committed
Add support for PS* algorithms with PyCryptodome
1 parent 4934c86 commit ebcbed4

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-3
lines changed

jwt/contrib/algorithms/pycryptodome.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import Cryptodome.Hash.SHA384
33
import Cryptodome.Hash.SHA512
44
from Cryptodome.PublicKey import ECC, RSA
5-
from Cryptodome.Signature import DSS, PKCS1_v1_5
5+
from Cryptodome.Signature import DSS, PKCS1_v1_5, pss
66

77
from jwt.algorithms import Algorithm
88
from jwt.compat import string_types, text_type
@@ -61,7 +61,6 @@ def __init__(self, hash_alg):
6161
self.hash_alg = hash_alg
6262

6363
def prepare_key(self, key):
64-
6564
if isinstance(key, ECC.EccKey):
6665
return key
6766

@@ -88,3 +87,39 @@ def verify(self, msg, key, sig):
8887
return True
8988
except ValueError:
9089
return False
90+
91+
92+
class RSAPSSAlgorithm(RSAAlgorithm):
93+
"""
94+
Performs a signature using RSASSA-PSS with MGF1
95+
96+
This class requires the PyCryptodome package to be installed.
97+
"""
98+
99+
def prepare_key(self, key):
100+
if isinstance(key, ECC.EccKey):
101+
return key
102+
103+
if isinstance(key, string_types):
104+
if isinstance(key, text_type):
105+
key = key.encode("utf-8")
106+
key = RSA.import_key(key)
107+
else:
108+
raise TypeError("Expecting a PEM- or RSA-formatted key.")
109+
110+
return key
111+
112+
def sign(self, msg, key):
113+
signer = pss.new(key)
114+
hash_obj = self.hash_alg.new(msg)
115+
return signer.sign(hash_obj)
116+
117+
def verify(self, msg, key, sig):
118+
hash_obj = self.hash_alg.new(msg)
119+
verifier = pss.new(key)
120+
121+
try:
122+
verifier.verify(hash_obj, sig)
123+
return True
124+
except (ValueError, TypeError):
125+
return False

tests/contrib/test_algorithms.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
try:
2424
# fmt: off
25-
from jwt.contrib.algorithms.pycryptodome import RSAAlgorithm, ECAlgorithm # noqa: F811
25+
from jwt.contrib.algorithms.pycryptodome import RSAAlgorithm, ECAlgorithm, RSAPSSAlgorithm # noqa: F811
2626
# fmt: on
2727

2828
has_pycryptodome = True
@@ -406,3 +406,42 @@ def test_ec_prepare_key_should_be_idempotent(self):
406406
jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first)
407407

408408
assert jwt_pub_key_first == jwt_pub_key_second
409+
410+
def test_rsa_pss_sign_then_verify_should_return_true(self):
411+
algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256)
412+
413+
message = force_bytes("Hello World!")
414+
415+
with open(key_path("testkey_rsa"), "r") as keyfile:
416+
priv_key = algo.prepare_key(keyfile.read())
417+
sig = algo.sign(message, priv_key)
418+
419+
with open(key_path("testkey_rsa.pub"), "r") as keyfile:
420+
pub_key = algo.prepare_key(keyfile.read())
421+
422+
result = algo.verify(message, pub_key, sig)
423+
assert result
424+
425+
def test_rsa_pss_verify_should_return_false_if_signature_invalid(self):
426+
algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256)
427+
428+
jwt_message = force_bytes("Hello World!")
429+
430+
jwt_sig = base64.b64decode(
431+
force_bytes(
432+
"ywKAUGRIDC//6X+tjvZA96yEtMqpOrSppCNfYI7NKyon3P7doud5v65oWNu"
433+
"vQsz0fzPGfF7mQFGo9Cm9Vn0nljm4G6PtqZRbz5fXNQBH9k10gq34AtM02c"
434+
"/cveqACQ8gF3zxWh6qr9jVqIpeMEaEBIkvqG954E0HT9s9ybHShgHX9mlWk"
435+
"186/LopP4xe5c/hxOQjwhv6yDlTiwJFiqjNCvj0GyBKsc4iECLGIIO+4mC4"
436+
"daOCWqbpZDuLb1imKpmm8Nsm56kAxijMLZnpCcnPgyb7CqG+B93W9GHglA5"
437+
"drUeR1gRtO7vqbZMsCAQ4bpjXxwbYyjQlEVuMl73UL6sOWg=="
438+
)
439+
)
440+
441+
jwt_sig += force_bytes("123") # Signature is now invalid
442+
443+
with open(key_path("testkey_rsa.pub"), "r") as keyfile:
444+
jwt_pub_key = algo.prepare_key(keyfile.read())
445+
446+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
447+
assert not result

0 commit comments

Comments
 (0)