Search code examples
javascriptnode.jsjwtgoogle-cloud-kms

Can't generate valid jwt signature with google kms


I am using Google KMS (https://cloud.google.com/kms/) with an asymmetric signing key to sign JSON Web tokens (jwt) in a node.js application.

I am able to create header and payload and with the Google KMS nodejs library (https://github.com/googleapis/nodejs-kms) I can sign the token.

But it seems that the generated token is not valid.

In fact I am doing the following steps to generate a token:

  1. define jwt-header as object
  2. jwt-header object to string
  3. base64url encode jwt-header-string
  4. define jwt-payload as object
  5. jwt-payload object to string
  6. base64url encode jwt-payload-string
  7. concatenate (header.payload)
  8. decode string to base64 byte array
  9. create sha256 hash on byte array
  10. select asymmetric key from google kms
  11. use asymmetric key to get the signature byte array
  12. use base64url encoding on the signature
  13. build token (header.payload.signature)

This token alway brings the error (invalid signature). See https://jwt.io/

The public key to verify the token:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmv3Slq7ofruWhFRXmnvt
/4WQuoJtoF2UQMtXmsZAZSODalklN21lV9t1ECAXOyOQX7E2QF8RZfJow5ImeTM5
WWHDhvFcg1/rI6bPIkkp4+Lu4Ljo8IIfYkEbIHt8+yOumEqiA1cvBR1TbojHMl4C
XW8jS4y4g7U6ZNYqKxOh9yvX6yUE0WRSzffRNVvx+Z5SNpmyjOXH/8A8e9BpG8tl
tAwdbtLd7Z+hcr60IERSWgqxnzUwzFWqdo4VNgLG68b1lKocbL8f0SnZiG0huyh0
tUEntR7PFWDePc2fOJmY9N9phgoD5FQjUQQiNipZi/Jw/z/BUz+utmQHwHNqyvCQ
ZQIDAQAB
-----END PUBLIC KEY-----

The code to build the jwt:

const kms = require('@google-cloud/kms');
const crypto = require('crypto');
const base64url = require('base64url');

async function main() {
    const client = new kms.KeyManagementServiceClient({
        keyFilename: "./googleCloudKey.json"
    });

    const projectId = '...';
    const locationId = 'europe';
    const keyRingId = '...';
    const cryptoKeyId = '...';
    const cryptoKeyVersion = '1';

    const header = base64url(JSON.stringify({
        "alg": "RS256",
        "typ": "JWT",
        "issuer": "login.myapp.com",
        "audience": "*.myapp.com"
    }));
    const payload = base64url(JSON.stringify({
        "userId": "1234567890",
        "userName": "John Doe"
    }));

    const digest = crypto.createHash("sha256").update(Buffer.from(`${header}.${payload}`, "base64")).digest("base64");
    const name = client.cryptoKeyVersionPath(
        projectId,
        locationId,
        keyRingId,
        cryptoKeyId,
        cryptoKeyVersion
    );

    try {
        const result = await client.asymmetricSign({
            name: name, 
            digest: {
                sha256: digest
            }
        });

        const signature = base64url.fromBase64(result[0].signature.toString("base64"));

        console.log("====== HEADER =====");
        console.log(header);
        console.log();
        console.log("====== PAYLOAD =====");
        console.log(payload);
        console.log();
        console.log("====== SIGNATURE =====");
        console.log(signature);
        console.log();
        console.log();
        console.log("===== JWT =====");
        console.log(`${header}.${payload}.${signature}`);
    } catch(e) {
        console.error(e);
    }
}


main().catch(console.error);

The generated token looks like the following (= output of the last console.log):

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImlzc3VlciI6ImxvZ2luLm15YXBwLmNvbSIsImF1ZGllbmNlIjoiKi5teWFwcC5jb20ifQ.eyJ1c2VySWQiOiIxMjM0NTY3ODkwIiwidXNlck5hbWUiOiJKb2huIERvZSJ9.WVdM2NT5IGYKuCMV393yD7grA4GyVIrorL2OF-MHcRZESwPC3bOZIsx254IkMDInFyui74N6qEpHIe6UpR1JeuojMaGEANvSE0TtFpYgykU7xORmVEsjuZSYyKeEaTPAMwmXVPEKi5gQA9qlfQjTXE-h1xWYt2N3-pj2IHcgpgC-tarN1_TLNxZ5it2TrfpfGztI13L5WHYEFidExde9sxasvJsHZR3ax0wnoPn9V9rfqdXrEtG6-cdi9PAQprQClVOETtvpZNcCZpIlciHsaYBla5JjowbUmecSjQ54F-CuOggxvGvy16uG9p93ETlUyAknPTCGaMf9URyKkssYaw

Solution

  • In this snippet:

    Buffer.from(`${header}.${payload}`, "base64")
    

    You're base64-decoding the concatenation of header and payload. But the JWS spec doesn't call for that. https://www.rfc-editor.org/rfc/rfc7515#section-5.1