Skip to content
This repository has been archived by the owner on May 5, 2023. It is now read-only.

Cannot verify a JWT token via EC Key #5228

Closed
presidioshuta opened this issue Mar 29, 2022 · 3 comments
Closed

Cannot verify a JWT token via EC Key #5228

presidioshuta opened this issue Mar 29, 2022 · 3 comments

Comments

@presidioshuta
Copy link

I've successfully created/signed/verified an RSA Key but cannot do so for an EC Key using a JWT (I'm getting a false result). I'm guessing that a base64 operation may to blame, but I'm not sure.

examples.ts:

const index = require('./index');
const jwt = require('./jwt');

async function runExamples() {

    // 1. Get a connected KeyVault client
    const keyClient = await index.getKeyClient();

    console.log('keyClient is ', keyClient);

    // 2. Generate new EC Key
    const newEcKey = await index.createEcKey('mynewkey');

    console.log('newEcKey id is', newEcKey.key.kid);

    // 3. Create JWT using an EC Key
    const newJwt = await jwt.createJWTFromEcKey(newEcKey.key.kid, new Date().getTime().toString());

    console.log('newJwt is', newJwt);

    // 4. Verify our JWT with our EC Key
    const result = await jwt.verifyJWTFromEcKey(newJwt, newEcKey.key.kid);

    console.log('verification result is', result);
    
}

runExamples();

index.ts:

require('dotenv').config()
const { ClientSecretCredential } = require("@azure/identity");
const { KeyClient, CryptographyClient } = require("@azure/keyvault-keys");

import * as logger from '@presidioidentity/logger';


const AZURE_KEYVAULT_DISPLAY_NAME = process.env.AZURE_KEYVAULT_DISPLAY_NAME;
const AZURE_KEYVAULT_AUTH_ACCESS_KEYNAME = process.env.AZURE_KEYVAULT_AUTH_ACCESS_KEYNAME;
const AZURE_KEYVAULT_TENANT_ID = process.env.AZURE_KEYVAULT_TENANT_ID;
const AZURE_KEYVAULT_APP_ID = process.env.AZURE_KEYVAULT_APP_ID;
const AZURE_KEYVAULT_PASSWORD = process.env.AZURE_KEYVAULT_PASSWORD;

logger.log.info ({
    AZURE_KEYVAULT_DISPLAY_NAME,
    AZURE_KEYVAULT_TENANT_ID,
    AZURE_KEYVAULT_APP_ID,
    AZURE_KEYVAULT_PASSWORD
});

if (!AZURE_KEYVAULT_DISPLAY_NAME      ||
    !AZURE_KEYVAULT_TENANT_ID ||
    !AZURE_KEYVAULT_APP_ID    ||
    !AZURE_KEYVAULT_PASSWORD
) {
    throw Error('Required Azure env vars not found.')
}

let client;

function getKeyClient() {
    // TODO: cache `client`
    const credential = new ClientSecretCredential(
        AZURE_KEYVAULT_TENANT_ID,
        AZURE_KEYVAULT_APP_ID,
        AZURE_KEYVAULT_PASSWORD
    );

    const url = `https://${AZURE_KEYVAULT_DISPLAY_NAME}.vault.azure.net`;
  
    const client = new KeyClient(url, credential);

    return client;
}

function getCryptoClient(keyName: string) {
    const credential = new ClientSecretCredential(
        AZURE_KEYVAULT_TENANT_ID,
        AZURE_KEYVAULT_APP_ID,
        AZURE_KEYVAULT_PASSWORD
    );

    return new CryptographyClient(keyName, credential);
}


// create an EC Key
async function createEcKey(name: string, curve?: string) {
    if (!curve) curve = "P-256";
    const client = getKeyClient();
    return await client.createEcKey(name, { curve });
}

jwt.ts:

import * as logger from '@presidioidentity/logger';
const base64url = require('base64url');
const crypto = require('crypto');
const util = require('util');
const index = require('./index');

const { SignatureAlgorithm } = require("@azure/keyvault-keys");




// async signJsonWebToken(data: object, expiry: string): Promise<string> {
//     const headerB64 = this.base64url(JSON.stringify(this.keyHeader), 'binary');
//     const payloadB64 = this.base64url(this.getTokenData(data, expiry), 'utf8');
//     const payload = `${headerB64}.${payloadB64}`;
  
//     const key = await this.keyClient.getKey(this.KEY_NAME);
//     const cryptClient = new CryptographyClient(key, new DefaultAzureCredential());
  
//     const hash = crypto.createHash('sha256');
//     const digest = hash.update(payload).digest();
  
//     const signResult = await cryptClient.sign('RS256', digest);
//     const signResultB64 = this.base64url(signResult.result.toString(), 'utf8');
//     const result = `${payload}.${signResultB64}`;
//     this.logger.log('Key: ' + key.key);
//     this.logger.log('Sign result: ' + result);
//     return result;
//   }
  
  
function getTokenData(data: object, expiry: string): string {
    const now = Date.now();
    const expiresIn = new Date();
    if (expiry.endsWith('d')) {
        expiresIn.setDate(expiresIn.getDate() + parseInt(expiry));
    } else if (expiry.endsWith('h')) {
        expiresIn.setHours(expiresIn.getHours() + parseInt(expiry));
    } else if (expiry.endsWith('m')) {
        expiresIn.setMinutes(expiresIn.getMinutes() + parseInt(expiry));
    }
    const tokenData = Object.assign({
        iat: now,
        exp: expiresIn.getTime()
    }, data);
    return JSON.stringify(tokenData);
}

const ecKeyAlgo = 'ES256';

// create JWT using a KeyVault key
async function createJWTFromEcKey(keyId: string, expiry: string, header?: any, payload?: any) {
    const keyClient = await index.getKeyClient();

    if (!header) {
        header = { compact: true, typ: "jwt" };  
    }

    if (!payload) {
        const numericDateNow = Math.round((new Date()).getTime() / 1000);
        const ttl = 12 * 60 * 60;
        payload = {
            iss: 'pi-bank-dev',
            sub: 'pi-bank-dev',
            aud: 'https://auth.dev.presidioidentity.net/token',
            exp: numericDateNow + ttl,
            nbf: numericDateNow,
            iat: numericDateNow,
        };
    }

    const headerB64 = base64url(JSON.stringify(header), 'binary');
    const payloadB64 = base64url(getTokenData(payload, expiry), 'utf8');
    payload = `${headerB64}.${payloadB64}`;

    const cryptoClient = index.getCryptoClient(keyId);
    const hash = crypto.createHash('sha256');
    const digest = hash.update(payload).digest();
    // const signResult = await cryptoClient.signData(ecKeyAlgo, digest);         Not sure which we want
    const signResult = await cryptoClient.sign(ecKeyAlgo, digest);
    console.log('signResult', signResult);
    const signResultB64 = base64url(signResult.result.toString(), 'utf8');
    console.log('signResultB64', signResultB64);
    const result = `${payload}.${signResultB64}`;

    logger.log.info('Sign result: ' + result);
    return result;
}

// verify JWT using KeyVault
async function verifyJWTFromEcKey(JWT: any, keyId: string) {
    const cryptoClient = index.getCryptoClient(keyId);
    console.log('cryptoClient', cryptoClient);
    const jwtHeader = JWT.split('.')[0];
    const jwtPayload = JWT.split('.')[1];
    const jwtSignature = JWT.split('.')[2];
    // logger.log.debug({
    //     jwtHeader,
    //     jwtPayload,
    //     jwtSignature
    // });
    const signature = base64url.toBuffer(jwtSignature)
    const data = util.format('%s.%s', jwtHeader, jwtPayload);
    const hash = crypto.createHash('sha256');
    const digest = hash.update(data).digest()
    return await cryptoClient.verify(ecKeyAlgo, digest, signature);
}

module.exports = {
    createJWTFromEcKey,
    verifyJWTFromEcKey
}
@maorleger
Copy link
Member

maorleger commented Mar 31, 2022

Hi @presidioshuta - thank you for opening this issue. Just FYI for future reference I see you're using @azure/keyvault-keys which is hosted at https://github.com/azure/azure-sdk-for-js - so feel free to create any future issues there.

But I can help you with this - it does look related to base64url and while I'm not too familiar with base64url library I took your code snippet and got it working by using plain base64:

  let digest = crypto.createHash("sha256").update(payload).digest();
  const signResult = await cryptoClient.sign(ecKeyAlgo, digest);
  const signResultB64 = Buffer.from(signResult.result).toString("base64"); // base64, not base64url
  const jwt = `${payload}.${signResultB64}`;

  const jwtSignature = jwt.split(".")[2];
  const signature = Buffer.from(jwtSignature, "base64"); // use base64 as the encoding when creating the buffer
  digest = crypto.createHash("sha256").update(Buffer.from(payload)).digest();
  console.log("VERIFY RESULT", await cryptoClient.verify(ecKeyAlgo, digest, signature));

Hope this helps! I'm far from an expert in this area but it does not look related to the KeyVault client library so I think focusing on being able to successfully encode and decode using base64 or base64url without losing the padding would likely be a good path forward...

@maorleger
Copy link
Member

maorleger commented Mar 31, 2022

Experimenting a little bit more with the base64url library the following works as well, but is likely not the most elegant solution (it might be doing too many unnecessary conversions but it gives a starting point):

  let digest = crypto.createHash("sha256").update(payload).digest();
  const signResult = await cryptoClient.sign(ecKeyAlgo, digest);
  const signResultB64 = base64url.encode(Buffer.from(signResult.result));
  const jwt = `${payload}.${signResultB64}`;
  ...
  const jwtSignature = jwt.split(".")[2];
  const signature = Buffer.from(base64url.toBuffer(jwtSignature));
  console.log("VERIFY RESULT", await cryptoClient.verify(ecKeyAlgo, digest, signature));

@presidioshuta
Copy link
Author

Prepare for my L O V E

We are cooking with fire now. Thank you!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants