英文:
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?
问题
在使用JavaScript中浏览器内置的Crypto Subtle来签署消息时,需要签署编码消息的哈希值还是编码消息本身?
我之所以提出这个问题,是因为根据以下内容:
https://crypto.stackexchange.com/questions/15295/why-the-need-to-hash-before-signing-small-data
如果在签名之前不对数据进行哈希,那么无法获得一致的签名算法,因为您只能签署一定大小的消息,如果消息大小过大,就需要进行哈希。但这对于签名方案来说并不是一个好的实践。更重要的是,当数据未经哈希时,存在可以轻松伪造的签名方案,比如RSA,请参见我的回答。为了确保与签名消息的大小无关的安全性,我们通常使用哈希-然后-签名范式,即在执行签名操作之前对纯文本消息进行哈希处理,因此签名算法适用于任何消息大小,我们实际上无需关心消息大小。
然而,当我查看Mozilla网站上的示例代码时:
https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign
它显示了以下示例代码:
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
);
正如您所看到的,Mozilla的示例代码签署了编码消息而不是编码消息的哈希值。他们是错的吗?
示例代码确实在algorithm
参数中指定了hash: { name: "SHA-384" }
。那么,sign
函数是否会自动在签名之前对其进行哈希处理?这是否意味着我可以跳过自己进行哈希处理?
英文:
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?
答案1
得分: 1
WebCrypto 隐式地对消息进行哈希处理,即不能执行显式的哈希操作,否则会导致双重哈希。这也符合 WebCrypto 在 ECDSA 文档 中的描述,该文档参考了 FIPS-186(第 6.4.1 节),其中将消息的哈希处理定义为签名过程的一部分。
与此一致的是,在 WebCrypto 文档的 ECDSA 示例 中,将消息而不是消息哈希传递给了 sign()
函数。
最终的验证需要使用受信任的库来进行。由于 WebCrypto 支持 ECDSA 的非确定性变体,因此每次都会生成不同的签名(即使使用相同的密钥和消息)。因此,只能通过成功地“验证”签名来进行验证。
一个可能的验证库是 elliptic,与 WebCrypto 不同,elliptic 不会隐式哈希消息,因此需要执行显式的哈希处理(文档和示例中也有描述)。使用 elliptic,可以成功验证使用 WebCrypto 生成的签名,如以下代码所示:
(async () => {
// 使用 WebCrypto 进行签名:WebCrypto 隐式哈希处理:
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); // 传递消息
var rawPublicKeyAB = await window.crypto.subtle.exportKey("raw", keyPair.publicKey);
// 使用 elliptic 进行验证:elliptic 不会隐式哈希处理,因此需要显式哈希处理:
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); // 哈希消息
var verified = publicKey.verify(msgHashHex, signatureJO); // 传递消息哈希
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>
英文:
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:
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
(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('');
}
})();
<!-- language: lang-html -->
<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>
<!-- end snippet -->
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论