1

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?

5
  • 1
    Yes, you can skip the explicit hashing. WebCrypto implicitly hashes the data during signing using the digest specified with the algorithm parameter (here SHA-384). Many libraries do this (to avoid signing unhashed data). Commented May 28, 2023 at 6:05
  • @Topaco is this documented somewhere? If you post it as an answer, I will accept it. Commented May 28, 2023 at 8:09
  • 2
    It is described indirectly by reference to FIPS-186 (in the WebCrypto docs) and to ANS X9.62 (in FIPS-186): The signature generation process consists of: 1. Message digesting (...) Compute the hash value e = H(M) using the hash function.... This is consistent with the ECDSA example from the Webrypto docs that passes the message (and not the message hash). And ultimately, this can be confirmed by verifying with another (reliable) library. Commented May 28, 2023 at 10:48
  • Thanks. If you post it as an answer, I will accept it. Commented May 28, 2023 at 21:19
  • You're welcome. I posted my comment as answer. Commented May 31, 2023 at 6:25

1 Answer 1

1

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>

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.