2

I'm trying to verify a signature generated with Google's cloud KMS, but I keep getting invalid responses.

Here's how I'm testing it:

const versionName = client.cryptoKeyVersionPath(
      projectId,
      locationId,
      keyRingId,
      keyId,
      versionId
    )

    const [publicKey] = await client.getPublicKey({
      name: versionName,
    })

    const valueToSign = 'hola, que tal'

    const digest = crypto.createHash('sha256').update(valueToSign).digest()

    const [signResponse] = await client.asymmetricSign({
      name: versionName,
      digest: {
        sha256: digest,
      },
    })

    const valid = crypto.createVerify('sha256').update(digest).verify(publicKey.pem, signResponse.signature)

    if (!valid) return console.log('INVALID SIGNATURE')

    console.log('SIGNATURE IS VALID!')

// output: INVALID SIGNATURE

This code will always log 'INVALID SIGNATURE' unless I use the original message instead of its hash:

const valid = crypto.createVerify('sha256').update(valueToSign).verify(publicKey.pem, signResponse.signature) // true

But using a local private key, I'm able to sign messages and verify them using their hashes:

const valueToSign = 'hola, the tal'
const msgHash = crypto.createHash("sha256").update(valueToSign).digest('base64');

const signer = crypto.createSign('sha256');
signer.update(msgHash);
const signature = signer.sign(pk, 'base64');

const verifier = crypto.createVerify('sha256');
verifier.update(msgHash);
const valid = verifier.verify(pubKey, signature, 'base64');
console.log(valid) // true

Why is it? Is there something different about kms signatures?

Topaco
  • 40,594
  • 4
  • 35
  • 62
AFMeirelles
  • 409
  • 3
  • 8
  • 25

2 Answers2

1

Based on this example from the crypto module documentation and your observations, I'd say that you might've misunderstood how client.asymmetricSign works. Let's analyze what happens:

Your local private key code:

const valueToSign = 'hola, the tal'

// Create sha256 hash
const msgHash = crypto.createHash("sha256").update(valueToSign).digest('base64');

// Let signer sign sha256(hash)
const signer = crypto.createSign('sha256');
signer.update(msgHash);
const signature = signer.sign(pk, 'base64');
// We now got sign(sha256(hash))

// Let verifier verify sha256(hash)
const verifier = crypto.createVerify('sha256');
verifier.update(msgHash);

const valid = verifier.verify(pubKey, signature, 'base64');
console.log(valid) // true

We are verifying sign(sha256(hash)) using verify(sha256(hash)).


Your KMS code:

const valueToSign = 'hola, que tal'

// Create sha256 hash
const digest = crypto.createHash('sha256').update(valueToSign).digest()

// Let KMS sign the hash
const [signResponse] = await client.asymmetricSign({
    name: versionName,
    digest: {
        sha256: digest, // we already say "we hashed our data using sha256"
    },
});
// We now got `sign(hash)`, NOT `sign(sha256(hash))` (where hash == digest)

// Let verifier verify sha256(hash)
const valid = crypto.createVerify('sha256').update(digest).verify(publicKey.pem, signResponse.signature)

We are verifying sign(hash) using verify(sha256(hash)).


Basically, locally you are signing your hash and verifying the signed hash. With KMS you are signing your data and verifying the signed hash, which is actually your signed data, hence your 2nd attempt with .update(valueToSign) works.

Solution? Hash your sha256 hash again before letting KMS sign it, since KMS expects the sha256 hash of the to-be-signed data, while the crypto expects the to-be-signed data (which it'll hash itself given the algorithm you passed to createSign).

Kelvin Schoofs
  • 8,323
  • 1
  • 12
  • 31
  • Thanks for the detailed answer. I was actually misunderstanding how crypto works, but now it's clear. 150 points to you! – AFMeirelles Aug 13 '21 at 02:00
1

The answer is very similar to the one from Kevin but from a different point of view, in other words.

When you use crypto.createSign(<algorithm>) and crypto.createVerify(<algorithm>) you are indicating the digest algorithm that will be used for signature creation and verification, respectively.

When you call update on the returned Sign and Verify objects you need to provide your data as is, crypto will take care of digesting that information as appropriate when you sign or verify it later.

In contrast, the GCP KMS asymmetricSign operation requires a message digest produced with the designated algorithm over your original data as argument. This is why you need to calculate the message digest with crypto.createHash first.

But please, as indicated, be aware that this fact doesn't change the behavior of the crypto verification process, it always requires the original data as input, this is why your code works when you pass your original data without hashing.

Although you provided a working example in your question for reference the GCP documentation provides additional ones.

jccampanero
  • 50,989
  • 3
  • 20
  • 49