For my own understanding of how verifying the signature of a JWT works I tried reimplementing the example given in appendix A.2 of RFC7515, the RFC that defines the JSON web signature (or JWS). https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2
I'm using python 3.9 and the pycryptodome library. I've managed to reproduce the octet sequence they derive for the JWS Signing Input value and I'm pretty sure I've also successfully converted their RSA key description to the valid Crypto.PublicKey.RSA
object. However, my value for the signature does not match what they have and I have no idea where I am making a mistake. If anyone could help me here, I would really appreciate it!
My code is as follows
import base64
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
header = '{"alg":"RS256"}'
body = '{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}'
enc_header = base64.urlsafe_b64encode(header.encode("utf-8"))
enc_body = base64.urlsafe_b64encode(body.encode("utf-8"))
enc = enc_header + b"." + enc_body
enc = enc[:-2] # Drop b"=="
print("JWS Signing Input value:", [int(x) for x in enc]) # Matches the IETF example
# The following were extracted from their RSA key by applying for each value:
# value -> int.from_bytes(bytes=base64.urlsafe_b64decode(value + b"=" * (-len(value) % 4)), byteorder='big')
n = 20446702916744654562596343388758805860065209639960173505037453331270270518732245089773723012043203236097095623402044690115755377345254696448759605707788965848889501746836211206270643833663949992536246985362693736387185145424787922241585721992924045675229348655595626434390043002821512765630397723028023792577935108185822753692574221566930937805031155820097146819964920270008811327036286786392793593121762425048860211859763441770446703722015857250621107855398693133264081150697423188751482418465308470313958250757758547155699749157985955379381294962058862159085915015369381046959790476428631998204940879604226680285601
e = 65537
d = 2358310989939619510179986262349936882924652023566213765118606431955566700506538911356936879137503597382515919515633242482643314423192704128296593672966061810149316320617894021822784026407461403384065351821972350784300967610143459484324068427674639688405917977442472804943075439192026107319532117557545079086537982987982522396626690057355718157403493216553255260857777965627529169195827622139772389760130571754834678679842181142252489617665030109445573978012707793010592737640499220015083392425914877847840457278246402760955883376999951199827706285383471150643561410605789710883438795588594095047409018233862167884701
p = 157377055902447438395586165028960291914931973278777532798470200156035267537359239071829408411909323208574959800537247728959718236884809685233284537349207654661530801859889389455120932077199406250387226339056140578989122526711937239401762061949364440402067108084155200696015505170135950332209194782224750221639
q = 129921752567406358990993347540064445018230073402482260994179328573323861908379211274626956543471664997237185298964648133324343327052852264060322088122401124781249085873464824282666514908127141915943024862618996371026577302203267804867959037802770797169483022132210859867700312376409633383772189122488119155159
ietf_key = RSA.construct(
rsa_components=(n, e, d, p, q)
)
signer = pkcs1_15.new(ietf_key)
h = SHA256.new(enc)
signature = signer.sign(h)
pkcs1_15.new(ietf_key).verify(h, signature)
print([int(x) for x in signature]) # Does not match the IETF example
print(base64.urlsafe_b64encode(signature))
You get the result from RFC 7515, A.2.1. Encoding if you use for
body
:The difference to the value you used is a
0x0d0a20
byte sequence (\r\n<space>
) after each comma. This is described in A.1.1. Encoding.