JWT With RSA Signing

I’ve lost count of the number of times I’ve advised companies to stop using HMAC in signing of JWT tokens. Why? Because they often just use…

JWT With RSA Signing

I’ve lost count of the number of times I’ve advised companies to stop using HMAC in signing of JWT tokens. Why? Because they often just use a simple password to generate the HMAC key, and where an intruder could discover the password and sign valid tokens. Along with this, every verifer also typically contains the secret passphrase, or where it can be discovered from analysing the application code. In one case, the developers were actually using “password” to generate the HMAC key — I discovered this by purely analysing the generated token. In one case, I also found the secret pass phrase that was used to generate the HMAC key on a public GitHub.

Overall, in many cases, RSA encryption for the signing is the most secure. This is especially true where we do not want to store our signing key on a service which checks the token. So, in this case, we will generate an RSA key pair, and sign the token with our private key, and then use the public key to verify it:

In this case, we have a JSON data format for the token with a subject, audience, and issuer (ISS), and where Bob signs with his private key, and then Alice proves the signer on the token with his public key. This method differs from HMAC signing, and where Bob and Alice share the same key:

The weakness of the HMAC method is where the secret key is generated by a pass phrase, and where Eve could discover the passphrase, whereas it is will more difficult for her to discover Bob’s private key from an RSA keypair (as Alice will not have a copy of this).

We can generate a random RSA key pair with a given key size (keysize) using:

from cryptography.hazmat.primitives.asymmetric import rsa 
private_key = rsa.generate_private_key(public_exponent=65537,key_size=keysize)
pub = private_key.public_key()

An example 512-bit key pair is:

RSA Key D: 873428901741531390243308088076802075409182503565445457430877461854491772510018020580772472745563960545467171740664254520496589101406260452990609047076713
RSA Key P: 113339934248588204507988976684339700323359810632008509572073140145274116777941
RSA Key Q: 104846639046521751390133254257908271593809806358531919599554408238767020809107
Private key (PEM):
-----BEGIN PRIVATE KEY-----
MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA4uRqQycnVohE7f5Q
1k+67ffP7AeMCFr2rC1dPYClZ6fmLRdXM88GvpFPNFR7ClMkO.druS7Oih2Be4tn
EbfWTwIDAQABAkAQrTtGPk185b0zRPsKFYgmz40fyxo2zwDMfPUafScrLB+hT2nx
zqwSTV8Y0sEg49xOmAqqhAUfCM1LZMJzJr9pAiEA+pQhJFVuNa4fO9X..zXQLOrZ
ORKNK28RyIal.OayJ9UCIQDnzRhATXjKopI1DVhCc8m+sXOWllUgoaqb4DhGuZmb
kwIgG+02ZF5BEip9wKVxCnhs4xSpcProUNboHHklNrJfWKECIGSR1V3AvxGbqzed
TJe4SOWVdAL3woNf4Pe0NnZo.D5FAiB3gFpmxMEit3164PTI1cXpP1IrDRcMDIqF
6Pt4yvTC5Q==
-----END PRIVATE KEY----

and where D is the decryption exponent, and P and Q are the random prime numbers. We can then sign with our private key (private_key) with:


token = jwt.encode({'aud':aud, 'exp':timeout, 'iss':issuer,'jti':id,
'sub':subject}, private_key, algorithm=signer)
print(f"Token: {token}\n")

and then decode and check the signature with the public key (pub):

rtn=jwt.decode(token, pub, algorithms=signer,audience=aud)
print(f"Decoded: {rtn}")

The full code is [here]:

import jwt
import time
import sys
from datetime import datetime, timedelta
from cryptography.hazmat.primitives import serialization
subject='test'
aud='test'
issuer='ASecuritySite'
id="123456"
signer='RS256'
keysize=512
if (len(sys.argv)>1):
subject=str(sys.argv[1])
if (len(sys.argv)>2):
aud=str(sys.argv[2])
if (len(sys.argv)>3):
issuer=str(sys.argv[3])
if (len(sys.argv)>4):
id=str(sys.argv[4])
if (len(sys.argv)>5):
signer=str(sys.argv[5])
if (len(sys.argv)>6):
keysize=int(sys.argv[6])

from cryptography.hazmat.primitives.asymmetric import rsa
private_key = rsa.generate_private_key(public_exponent=65537,key_size=keysize)
pub = private_key.public_key()

timeout = datetime.utcnow() + timedelta(seconds=3600)
print(f"Subject:\t{subject}")
print(f"Timeout:\t{timeout}")
print(f"Audience:\t{aud}")
print(f"Issuer:\t\t{issuer}")
print(f"ID:\t\t{id}")
print(f"Signer:\t\t{signer}")
print(f"Keysize:\t{keysize}")
print(f"\nRSA Key D:\t{private_key.private_numbers().d}")
print(f"RSA Key P:\t{private_key.private_numbers().p}")
print(f"RSA Key Q:\t{private_key.private_numbers().q}")
try:
print("\n=== Private Key PEM format ===")
pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8,encryption_algorithm=serialization.NoEncryption())
print ("\nPrivate key (PEM):\n",pem.decode())
token = jwt.encode({'aud':aud, 'exp':timeout, 'iss':issuer,'jti':id, 'sub':subject}, private_key, algorithm=signer)
print(f"Token: {token}\n")
rtn=jwt.decode(token, pub, algorithms=signer,audience=aud)
print(f"Decoded: {rtn}")
except Exception as error:
print(error)

A sample run gives [here]:

Subject:	hello
Timeout: 2024-02-04 09:15:14.615631
Audience: qwerty
Issuer: ASecuritySite
ID: 123456
Signer: RS256
Keysize: 512

RSA Key D: 873428901741531390243308088076802075409182503565445457430877461854491772510018020580772472745563960545467171740664254520496589101406260452990609047076713
RSA Key P: 113339934248588204507988976684339700323359810632008509572073140145274116777941
RSA Key Q: 104846639046521751390133254257908271593809806358531919599554408238767020809107
Private key (PEM):
-----BEGIN PRIVATE KEY-----
MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA4uRqQycnVohE7f5Q
1k+67ffP7AeMCFr2rC1dPYClZ6fmLRdXM88GvpFPNFR7ClMkO.druS7Oih2Be4tn
EbfWTwIDAQABAkAQrTtGPk185b0zRPsKFYgmz40fyxo2zwDMfPUafScrLB+hT2nx
zqwSTV8Y0sEg49xOmAqqhAUfCM1LZMJzJr9pAiEA+pQhJFVuNa4fO9X..zXQLOrZ
ORKNK28RyIal.OayJ9UCIQDnzRhATXjKopI1DVhCc8m+sXOWllUgoaqb4DhGuZmb
kwIgG+02ZF5BEip9wKVxCnhs4xSpcProUNboHHklNrJfWKECIGSR1V3AvxGbqzed
TJe4SOWVdAL3woNf4Pe0NnZo.D5FAiB3gFpmxMEit3164PTI1cXpP1IrDRcMDIqF
6Pt4yvTC5Q==
-----END PRIVATE KEY-----
Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJxd2VydHkiLCJleHAiOjE3MDcwMzgxMTQsImlzcyI6IkFTZWN1cml0eVNpdGUiLCJqdGkiOiIxMjM0NTYiLCJzdWIiOiJoZWxsbyJ9.fdPQuYA5wQjJc-XcHTnMO1CkkY_zEpJeB2gos5653HbXldAcC05_LcfBi1Gcn_AKw3OmqoHJIEW0LCcYwfFJIA
Decoded: {'aud': 'qwerty', 'exp': 1707038114, 'iss': 'ASecuritySite', 'jti': '123456', 'sub': 'hello'}

Conclusions

Remember, JWT’s are not encrypted by default — they are encoded! For signing, RSA signing is typically more secure than HMAC signaures, but you have to have a way to distribute (and revoke) a trusted public key. Here is more on JWTs:

https://asecuritysite.com/jwt/