Search code examples
javascriptnode.jsjwtcryptographyrs256

Tried to verify JWT signature by myself in nodejs to understand internal working of JWT, but decrypted signature gives wrong value


To understand how digital signatures and JWT work, I tried to verify a JSON Web Token using RS256 algorithm. However, when I decrypt the signature part of a JWT it gives non-string values, so I cannot compare the value with the calculated hash value. Can someone tell me what part I am misunderstanding in my code? I used a RS256 algorithm JWT token, and all values are given in https://jwt.io/. If you scroll down and select RS256 option, you can get base64url encoded JWT and public/private keys. I guess that I am decrypting the wrong part of the JWT, but cannot find it out.

const base64url = require('base64url')
const crypto = require('crypto')
const fs = require('fs')

function readKeyPair(path) {
    return {
        publicKey: fs.readFileSync(path.publicPath),
        privateKey: fs.readFileSync(path.privatePath)
    }
}

function encryptWithPrivateKey(privateKey, message) {
    const bufferMessage = Buffer.from(message, 'utf8');
    return crypto.privateEncrypt(privateKey, bufferMessage)
}

function decryptWithPublicKey(publicKey, buffer) {
    return crypto.publicDecrypt(publicKey, buffer);
}

function hashMessage(message, algorithm) {
    const hash = crypto.createHash(algorithm);
    hash.update(message);
    const hashValue = hash.digest('hex')
    return hashValue;
}

const JWT = (
    'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZS' +
    'I6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZ' +
    'S82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExR' +
    'EkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQG' +
    'xHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8Ocaar' +
    'A8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618i' +
    'Yv7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA'
)

const KEY_PAIR_PATH = {
    publicPath: 'rsa_pub.pem',
    privatePath: 'rsa_priv.pem'
}

const jwtParts = JWT.split('.')
const header = base64url.decode(jwtParts[0])
const payload = base64url.decode(jwtParts[1])
const signature = base64url.toBuffer(jwtParts[2])

console.log(header)
console.log(payload)
console.log(signature)

const keyPair = readKeyPair(KEY_PAIR_PATH)
const decryptedHashValue = decryptWithPublicKey(keyPair.publicKey, signature);

const newHash = hashMessage(jwtParts[0] + '.' + jwtParts[1], 'SHA256')
console.log()
console.log(decryptedHashValue.toString())
console.log()
console.log(newHash)

and this is output of the code

{"alg":"RS256","typ":"JWT"}
{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}
<Buffer 3c eb 2d 19 eb 5f 03 2b 5a 65 2f 36 c0 77 23 a1 3c a8 aa 13 32 c5 78 96 75 1e cd 9f b0 36 f4 33 52 97 41 22 5c b7 70 27 ac 42 e8 07 e0 65 61 75 6c eb ... 206 more bytes>

010     `�He �A����O��H7��Rb�'��!9���Ct�_S

8041fb8cba9e4f8cc1483790b05262841f27fdcb211bc039ddf8864374db5f53

Solution

  • The posted code UTF8 decodes the value of decryptedHashValue, corrupting the data and creating the gibberish. Arbitrary binary data such as ciphertexts or hash values must be converted to a string using a binary-to-text encoding, such as Base64 or hex encoding. Since hashMessage() hex encodes the data, hex encoding is the appropriate choice here:

    console.log(decryptedHashValue.toString('hex'))
    

    which gives the following output:

    3031300d0609608648016503040201050004208041fb8cba9e4f8cc1483790b05262841f27fdcb211bc039ddf8864374db5f53
    

    This value can be split into the following two parts:

    3031300d060960864801650304020105000420
    

    and

    8041fb8cba9e4f8cc1483790b05262841f27fdcb211bc039ddf8864374db5f53
    

    The second portion corresponds exactly to the hash value newHash determined with newHash(). The first portion corresponds to the digest ID of SHA-256, which is prepended in the case of PKCS1 v1.5 padding (RSASSA-PKCS1-v1_5, s. RFC8017), the default padding used by privateEncrypt() and publicDecrypt().

    However, I cannot reproduce the value you posted for the hash with the code you posted.