hashcryptographydigital-signatureecdsawebcrypto-api

When using Crypto Subtle in Javascript to sign a message, do we need to sign the hash of the encoded message or the encoded message itself?


When using the browser built in Crypto Subtle in Javascript to sign a message, do we need to sign the hash of the encoded message or the encoded message itself?

The reason I ask is because as per the following:

https://crypto.stackexchange.com/questions/15295/why-the-need-to-hash-before-signing-small-data

If you do not hash the data before signing you cannot have one consistent signature algorithm, because you could only sign messages up to a certain size and if the size of the message gets too large you would need to hash. But that is not a good practice for signature schemes. More importantly, there are signature schemes which can easily be forged when the data is not hashed, such as RSA, see my answer here. In order to have security independent of the size of the signed message, we typically use this hash-then-sign paradigm, i.e., hash the plain message before performing signing operations on it, and thus the signature algorithm works for any size of the message and we do not really have to care about the message size.

However, when I look at the example code on Mozilla's site:

https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign

It shows this example code:

function getMessageEncoding() {
  const messageBox = document.querySelector(".ecdsa #message");
  let message = messageBox.value;
  let enc = new TextEncoder();
  return enc.encode(message);
}

let encoded = getMessageEncoding();
let signature = await window.crypto.subtle.sign(
  {
    name: "ECDSA",
    hash: { name: "SHA-384" },
  },
  privateKey,
  encoded
);

As you can see, Mozilla's example code signs the encoded message instead of hash of the encoded message. Are they doing it wrong?

The example code does specify hash: { name: "SHA-384" } as part of the algorithm parameter. So, is the sign function automatically doing the hash before signing it? Does that mean I can skip having to do the hash myself?


Solution

  • WebCrypto hashes the message implicitly, i.e. no explicit hashing may be performed, otherwise double hashing would occur. This also follows from the WebCrypto documentation on ECDSA, which refers to FIPS-186 (sec. 6.4.1), where the hashing of the message is defined as part of the signing process.

    Consistent with this is that in the ECDSA example of the WebCrypto documentation, the message and not the message hash is passed to sign().

    The ultimate check is the verification with a trusted library. Since WebCrypto supports the non-deterministic variant of ECDSA, a different signature is generated each time (even using the same key and message). Therefore, verification is only possible by successfully verifying the signature.
    A possible library for verification is elliptic, which unlike WebCrypto does not implicitly hash, so that explicit hashing is required (which is also described in the documentation and examples). With elliptic, the signature generated with WebCrypto can be successfully verified, as the following code proves:

    (async () => {
    
        // Signing with WebCrypto: WebCrypto hashes implicitly:
        var messageStr = 'The quick brown fox jumps over the lazy dog';
        var messageAB = new TextEncoder().encode(messageStr);
        var keyPair = await window.crypto.subtle.generateKey({name: "ECDSA",namedCurve: "P-384"}, true, ["sign", "verify"]);
        var signatureAB = await window.crypto.subtle.sign({name: "ECDSA", hash: { name: "SHA-384" }}, keyPair.privateKey, messageAB); // pass the message
        var rawPublicKeyAB = await window.crypto.subtle.exportKey("raw", keyPair.publicKey);
    
        // Verification with elliptic: elliptic does not hash implicitly, so explicit hashing is required:
        var ec = new elliptic.ec('p384');
        var publicKey = ec.keyFromPublic(ab2hex(rawPublicKeyAB), 'hex');
        var signatureHex = ab2hex(signatureAB);
        var signatureJO = { r: signatureHex.substr(0, 96), s: signatureHex.substr(96,96) };
        var msgHashHex = sha384(messageStr);                                                                                                                                                                                    // hash the message
        var verified = publicKey.verify(msgHashHex, signatureJO);                                                                                                                                       // pass the message hash
        console.log("Verification:", verified);
    
        function ab2hex(ab) { 
            return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
        }
    
    })();
    <script src="https://cdnjs.cloudflare.com/ajax/libs/elliptic/6.5.4/elliptic.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/js-sha512/0.8.0/sha512.min.js"></script>