azure-keyvaultazure-sdk-js

azure keyvault : locally verify data signed by EC-HSM


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 '='


Solution

  • 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));