0

I made a test with Azure JS SDK to use key vault to sign and verify data thanks to EC-HSM

when I use Azure JS SDK to verify it works correctly, I put the output in comments

describe('crypto services', () => {
  it('Azure HSM', async () => {
    const client = new KeyClient(
      'https://xxx.vault.azure.net',
      new EnvironmentCredential(),
    );

    const keys: { [key: string]: KeyVaultKey } = {};
    for await (const key of client.listPropertiesOfKeys()) {
      keys[key.name] = await client.getKey(key.name);
    }
    console.log('available keys', Object.keys(keys)); //logging ['tmp']

    const cryptoClient = new CryptographyClient(
      keys['tmp'],
      new EnvironmentCredential(),
    );
    const content = Buffer.from('Hello world');
    const sig = await cryptoClient.signData('ES256', content);
    console.log('signature', Buffer.from(sig.result).toString('base64')); //logging different output on each call
    console.log('key crv', keys['tmp'].key.crv); //P-256
    console.log('key x', Buffer.from(keys['tmp'].key?.x).toString('base64')); //OC76WxZ/TMzJnRqv/cy9llDqSIMWlplgREY3jMxDCks=
    console.log('key y', Buffer.from(keys['tmp'].key?.y).toString('base64')); //Dxgw8nqXoO3xXQruejfQa/Z+aFpo/4ilC64JUHoRoog=

    const verif = await cryptoClient.verifyData('ES256', content, sig.result);
    expect(verif.result).toBe(true);
  });
})

Now, I'd like to verify locally without calling the azure sdk (which is exactly the reason to use asymmetric crypto in the first place)

  it.only('local HSM verif', () => {
    const content = Buffer.from('Hello world'); //same content
    const sigs = [ //some values I received
      'bxw1nS8Q39mnQVkAhvJctXuLHz4n0wUjLbE+phj1XlUeWDxl7DCK5bG4d7YrL7zGtAnUq3YT9AdrrAXjpwhCzQ==',
      'hlh1CJSKivFYcdVygI0KJfUCGYor+whUu6NsJZhNTCllHHRmgh9FvcvxBSFVu0am7A9lryG/N5vLAv/B1Niiew==',
    ];
    const base64X = 'OC76WxZ/TMzJnRqv/cy9llDqSIMWlplgREY3jMxDCks='; //same as output of previous test
    const base64Y = 'Dxgw8nqXoO3xXQruejfQa/Z+aFpo/4ilC64JUHoRoog=';//same as output of previous test
    //converting the jwk to PEM thanks to the jwk-to-pem package
    const publicKey = jwkToPem({
      kty: 'EC',
      crv: 'P-256',
      x: base64X,
      y: base64Y,
    });
    for (const sig of sigs) {
      const verifyLocal = crypto.createVerify('sha256');
      verifyLocal.update(content);        
      const verifLocal = verifyLocal.verify(publicKey, sig);
      expect(verifLocal).toBe(true); //it always fails here
    }
  });

Can someone explain what is going on ?

My guess is that the public signature is not correct, and so that x and y are not correct in base64

For ex, if I use mkjwk.org to generate a JWK

{
kty: 'EC',
d: '7LU9Y16XKiFMcPVm39B5fVOtG0s-bnJwaeEtMrk9udE',
crv: 'P-256',
x: 'g-68Nakmi41xMv6zKduBn4dqcqJ0KXDqdS2rFpxUQOA',
y: 'pYRqhl2YDKBwGGkIXdYQQWuNjOtPcCe1bz_VYalXFW0',
}

they seem shorter and without '/' or '='

Christophe Blin
  • 1,687
  • 1
  • 22
  • 40
  • If someone reads that one day :) 1. the public key is not the problem because az keyvauld download gives same PEM 2. nodejs can perfectly verify ecdsa keys so nodejs crypt is also not the probelm 3. in local HSM verif, you need to hash the content before verifying it because cryptoClient.signData does this, but it does not work either – Christophe Blin Jul 23 '21 at 07:06

1 Answers1

0

The issue turns out to be that NodeJS expects an ASN.1 DER signature while Azure Key Vault creates an IEEE-P1363 signature (aka concatenated signature).

Generating ECDSA signature with Node.js/crypto goes into details about this.

The following code snippet can be used to convert the concatenated signature to the format NodeJS expects (with some help from the jwk-to-pem, asn1.js, and bn.js packages):

import { CryptographyClient, KeyClient } from "@azure/keyvault-keys";
import { AzureCliCredential } from "@azure/identity";
import jwkToPem from "jwk-to-pem";
import * as crypto from "crypto";
import asn1 from "asn1.js";
import BN from "bn.js";

const EcdsaDerSig = asn1.define("ECPrivateKey", function (this: any) {
  return this.seq().obj(this.key("r").int(), this.key("s").int());
});

function concatSigToAsn1SigSig(concatSigBuffer: any) {
  const r = new BN(concatSigBuffer.slice(0, 32).toString("hex"), 16, "be");
  const s = new BN(concatSigBuffer.slice(32).toString("hex"), 16, "be");
  return EcdsaDerSig.encode({ r, s }, "der");
}

async function main() {
  const content = Buffer.from("Hello world"); // some content
  const client = new KeyClient(
    "https://<your_vault_name>.vault.azure.net",
    new AzureCliCredential()
  );
  const key = await client.createEcKey("eckeytest");

  const cryptoClient = new CryptographyClient(key, new AzureCliCredential());
  const signature = await cryptoClient.signData("ES256", content); // sign data remotely in Azure Key Vault
  const sig = concatSigToAsn1SigSig(signature.result); // convert sig to the node.js expected format

  const base64X = Buffer.from(key.key!.x!).toString("base64");
  const base64Y = Buffer.from(key.key!.y!).toString("base64");
  // convert JWK to PEM for verification
  const publicKey = jwkToPem({
    kty: "EC",
    crv: "P-256",
    x: base64X,
    y: base64Y,
  });

  // Depending on the version of Node used, you could use `crypto.verify` directly.
  const keyObj = crypto.createPublicKey(publicKey);
  let result = crypto.verify("sha256", content, keyObj, sig);
  console.log("Result using crypto.verify", result);

  // using a verifier which can be used directly with the `publicKey`
  let verify = crypto.createVerify("sha256");
  verify.update(content);
  verify.end();
  result = verify.verify(publicKey, sig);
  console.log("Result using crypto.createVerify", result);
}

main()
  .then(() => console.log("done"))
  .catch((err) => console.log(err));

mlnyc
  • 2,636
  • 2
  • 24
  • 29