JWTs: Reproducing the signing example from RFC7515

209 views Asked by At

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))

1

There are 1 answers

1
Topaco On BEST ANSWER

You get the result from RFC 7515, A.2.1. Encoding if you use for body:

body = '{"iss":"joe",\r\n "exp":1300819380,\r\n "http://example.com/is_root":true}'

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.