22

As I understand it, a JSON Web Token (JWT) consists of 3 parts:

  • the header, specifying the hashing algorithm to use for the signature;
  • the payload itself; and
  • the signature, which is a hash of the header and the payload using the specified hashing algorithm and a given secret.

The point of the signature is for the receiver to verify the integrity of the received JWT, that it has not been tampered with. This is done, presumably, by the receiver of the JWT reproducing the steps made by the JWT producer to create the signature, by hashing the header and the payload with the specified hashing algorithm and a given secret.

When the secret used by the JWT sender and receiver is one and the same (shared secret, symmetric key), I understand how this works. All the inputs are identical, so the hash will be identical, whether calculated by the sender or receiver.

My actual question, and what I don't understand, is how this works when the secrets used by the sender and receiver are different (asymmetric keys, public/private key pair). I.e. if the hash produced by the sender was generated using the secret key, and the hash produced by the receiver (for signature verification purposes) was produced by the corresponding public key.

How can two different secrets yield the same hash?

Squeamish Ossifrage
  • 48,392
  • 3
  • 116
  • 223

1 Answers1

21

I received the following explanation from a separate source.

Send:    { object | encrypt(hash(object), private_key) }
Receive: { object | signature }
Verify:  hash(object) == decrypt(signature, public_key)

This explains what I was struggling to understand.

There are two processes at work here: not just hashing, but also encryption. The actual hashing itself needs no secret parameter, it takes in only the object to be hashed (the concatenation of header + payload in the case of JWT). The resulting hash itself is then the input to the encryption, the result of which forms the signature, which can then be decrypted by the recipient to retrieve the original hash for comparison against the recipient's own computed hash.

Thus, instead of the sender and recipient performing similar calculations, with different secrets, to arrive at the same hash value:

hash(object, private_key) == signature == hash(object, public_key)

we rather have the sender and recipient performing complementary calculations to arrive back at the original input

decrypt(encrypt(hash_value, private_key), public_key) == hash_value

This makes sense with how I understand public-private key encryption.

  • Thanks for your explanation, but in public-private key encryption, we use public key to encrypt and private key to decrypt which opposite to what you said, decrypt(encrypt(hash_value, private_key), public_key), please correct me if I'm wrong. – sankar Mar 25 '19 at 22:14
  • 1
    For the normal encryption use case, where you want to obfuscate the message content so that only the recipient can read it, you would be correct: the sender would use the public key, and the receiver would use the private key. However, the use case here is that anyone should be able to read the token content, but need to be able to verify that the sender is the expected sender. For this use case, the sender uses the private key, and the receiver uses the public key. See the second paragraph of "What is JSON Web Token?" under https://jwt.io/introduction/. – Anders Rabo Thorbeck Apr 02 '19 at 12:12
  • @AndersRaboThorbeck the signature is hash (not encryption) which uses a sk to generate (HMACSHA256 needs a sk and that is the signature). So how can a client generate the same signature? what I am missing? – Mike.R Jan 21 '20 at 21:51
  • @Mike.R No, the signature is not a hash directly, it is the encryption of a hash, encrypted using the private key of a public-private key pair, or using a shared secret. The recipient of the JWT token does not generate the same signature, but rather decrypts the signature (using respectively the public key or the shared secret) to arrive back at the hash value, and can then verify that the hash value matches the content of the header and payload (by computing its own hash of these values and comparing it to the decrypted hash). – Anders Rabo Thorbeck Jan 22 '20 at 09:01
  • @AndersRaboThorbeck I understand the process that the receiver decrypts the signature with public key(and it was encrypted by sender) and try to campare it to his own hash, my question was about the hash. Your claim is that the receiver and the sender produce the same hash form payload + header BUT from jwt HMACSHA256(base64UrlEncode(header) + "."+base64UrlEncode(payload),SECRET) so the generated hash is depended on the sk (HMAC needs a secret key to generate hash), that what confuses me – Mike.R Jan 22 '20 at 10:01
  • @AndersRaboThorbeck sorry it just confused me but indeed I think u are right there is no sk involved in the hash generation – Mike.R Jan 22 '20 at 10:12
  • @Mike.R It might be misleading to call it a "hash", since it appears that it is reversible, but in the example you gave (taken from the official docs), the hash is literally just base64UrlEncode(header) + "." + base64UrlEncode(payload). It is a deterministic value derived only from the header and payload, and not dependent on any external secret or key. – Anders Rabo Thorbeck Jan 22 '20 at 12:44
  • Where does the receiver get the public key - is it passed in the JWT? – rangfu Jan 21 '21 at 08:53
  • 1
    you can either embed the pulic key in the "jwk" claim or use "jku" claim to point to a file url containing public keys (can be more than one), the use "kid" claim to match the key you need.

    see here for a really good explanation of everything: https://www.pingidentity.com/en/company/blog/posts/2019/jwt-security-nobody-talks-about.html

    – Dawesi Aug 08 '21 at 19:37