In my app I'm using JSON Web Tokens for authentication purposes, using the pyjwt library. Instead of using static keys and/or worrying about key distribution, the server generates a public/private key pair upon startup itself and just keeps it in memory. The keys will be reset and thereby all existing tokens invalidated when the server restarts, which is fine for the intended use case. The key pair is generated using pycrypto.
The question is: am I generating and using the keys correctly, or is there any flaw which can lead to an exploit? Below is the general class which generates and validates all JWTs in my app:
from typing import NamedTuple
from datetime import datetime
import jwt
from Crypto.PublicKey import RSA
KeyPair = NamedTuple('KeyPair', [('public', str), ('private', str)])
class JWT:
def __init__(self, keypair: KeyPair=None, algorithm: str='RS256'):
self.keypair = keypair or self.generate_keypair()
self.algorithm = algorithm
@staticmethod
def generate_keypair() -> KeyPair:
key = RSA.generate(2048)
return KeyPair(public=key.publickey().exportKey('PEM').decode('ascii'),
private=key.exportKey('PEM').decode('ascii'))
def generate_token(self, payload: dict) -> str:
payload.update(dict(iat=datetime.utcnow()))
return jwt.encode(payload, key=self.keypair.private, algorithm=self.algorithm).decode('ascii')
def decode_payload(self, token: str, **kwargs) -> dict:
try:
return jwt.decode(token, self.keypair.public, algorithms=[self.algorithm], **kwargs)
except jwt.exceptions.InvalidTokenError as e:
raise InvalidAuthenticationToken
1 Answer 1
There isn't too much to see here because the key generation simply relies on RSA.generate(2048)
, but I wonder why you would need this code as it is exceedingly shallow.
Regenerating key pairs for signing at startup is utter nonsense because a key pair is next to useless if the public key isn't trusted by the receiving party.
Exporting the private key as unprotected PEM is also very dangerous. The method would fail if hardware support (a HSM) is ever used.
The inclusion of payload.update(dict(iat=datetime.utcnow()))
is undocumented. Although it may be expected for webtokens, a caller will want to know that this is performed. Undocumented side effects are a sure fire way of breaking the principle of least surprise.
Passing kwargs**
without any validation doesn't give me a very warm feeling inside.
Slightly less of an issue maybe, but an RSA 2048 bit key provides a key strength of only 112 bits or so. You may want to consider at least 3072 bits.
This code is clearly related only to webtokens and RSA keys, even with a specific key size. So allowing an argument algorithm: str='RS256'
may just allow a user to get the object instance in an invalid state, and not much more.
-
\$\begingroup\$ Can you elaborate on the unprotected PEM HSM problem? \$\endgroup\$deceze– deceze2020年03月01日 13:06:41 +00:00Commented Mar 1, 2020 at 13:06
-
\$\begingroup\$ Well, basically you try and protect the private key to the best of your capabilities. Outputting a PEM string that is likely also exported (why would you otherwise) means that it is written to memory and to disk. A HSM or smart card can be used to protect a key value so that you can only use it on the device (a "token") itself, and commonly only after authentication. In that case PEM export (of the private key) would of course fail. \$\endgroup\$Maarten Bodewes– Maarten Bodewes2020年03月01日 15:37:16 +00:00Commented Mar 1, 2020 at 15:37
-
\$\begingroup\$ In my use case the key isn’t actually exported at all; I guess the major flaw in this use is using asymmetric encryption when symmetric encryption would do just fine. \$\endgroup\$deceze– deceze2020年03月01日 15:57:17 +00:00Commented Mar 1, 2020 at 15:57
-
\$\begingroup\$ That's OK though, better than the other way around :) \$\endgroup\$Maarten Bodewes– Maarten Bodewes2020年03月01日 16:01:53 +00:00Commented Mar 1, 2020 at 16:01
JWT
instances. \$\endgroup\$