JavaScript:一种实现电子签名的非常简单的方法。

7

在JS中是否有一种非常简单的方法来制作电子签名,使其可以像校验和(或哈希值)一样轻松处理?

所以如果情况是这样的:

------------------------------------
      Locked section for client
------------------------------------
| YYYY.MM.DD  ......................|
| ........... ......................|
| Bla bla bla ......................|
| Bla bla bla Bla bla bla..Bla bla .|
| Bla bla bla Bla bla bla..Bla bla .|
| Bla bla bla Bla bla bla..Bla bla .|
| Bla bla bla Bla bla bla..Bla bla .|
| Bla bla bla ......................|
| Bla bla bla ......................|
------------------------------------
|    HASH: HA2S2EM3CA12EDIAJED      |
------------------------------------
"Open" comment textfield for clients
------------------------------------
| HE34ADOV2DSASA452123 ...(signer A)|
| GHEAVOED12dHSAV2123J ...(signer B)|

HE34ADOV2DSASA452123是由签名者拥有的私钥生成的。

然后使用某种公钥对HE34ADOV2DSASA452123进行解密,会得到类似于YYYY.MM.DD Bla bla bla的内容或返回该部分的哈希值(HA2S2EM3CA12EDIAJED)。

同样地,对GHEAVOED12dHSAV2123J进行解密也会得到类似于YYYY.MM.DD Bla bla bla的内容或返回该部分的哈希值(HA2S2EM3CA12EDIAJED)。


请注意,这并不要求防范邪恶大脑,只需防范“外行”欺诈...


你能解释一下使用案例吗?你有一条消息和一些加密文本(可以解密为“YYYY.MM.DD Bla bla bla”)。有人知道它是如何加密的。你所说的“每个人都可以阅读”是什么意思? - Meredian
@Meredian 我更新了示例以更好地解释。 - Norfeldt
如果服务器存储盐值,那么任何可以更改数据的人都可以生成新的哈希值,因此您需要某种密钥固定方式。例如,在某个地方发布哈希值,以便记录每次更改。 - Jonas Wilms
除非需要人工验证,否则RSA签名应该足够。TweetNaCl JS是非常有名的。 - tsn
你到底想做什么?为什么不使用常见的哈希和公私钥程序,以及CryptoJS或SJCL?你到底缺少什么,哈希还是签名算法? - Lux
显示剩余2条评论
3个回答

5
电子签名的定义比哈希复杂得多。虽然您可以仅从消息中生成哈希,但对于数字签名,您通常需要一个私钥,并强制只有知道私钥的人才能生成有效签名。接下来,您显然需要相应的公钥来验证消息。
因此,通常有以下3个步骤:
1. 您需要创建公钥/私钥对。 2. 您需要在可信系统上使用私钥对消息进行签名。只有该系统应具有私钥。 3. 使用公钥验证消息,以确保可信系统已对其进行签名。
因此,第一个有趣的问题是,您想如何存储/分发密钥,并且要在哪种类型的系统上进行签名/验证?签名和验证使用不同的编程语言是一种常见用例。但是现在让我们假设您想在JavaScript中完成所有操作。
另外,请始终记住一个简单的问题: 如果您无法确定消息来自有效发送者,那么您如何确保用于验证消息的公钥来自有效发送者?您可以将其与软件一起分发,但对于网站,您必须信任TLS连接。如果您信任TLS连接,则也可以使用它来传输消息本身。
我认为最好的解决方案是使用Web Cryptography API这里您可以找到有用的示例。
首先,您需要生成密钥:
async function generateKey() {
  const key = await window.crypto.subtle.generateKey({
      name: "RSASSA-PKCS1-v1_5",
      modulusLength: 4096,
      publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
      hash: {
        name: "SHA-512"
      },
    },
    true,
    ["sign", "verify"]
  );

  return {
    privateKey: await window.crypto.subtle.exportKey(
      "jwk",
      key.privateKey,
    ),
    publicKey: await window.crypto.subtle.exportKey(
      "jwk",
      key.publicKey,
    ),
  };
}

window.crypto.subtle.exportKey可以给你一个JSON,你可以通过JSON.stringify/JSON.parse来将其转换为简单字符串并进行存储。


下一步是对您的消息进行签名。请注意,您可能希望在不同的时间生成密钥。

async function sign(privateKeyJwk, message) {
  const privateKey = await window.crypto.subtle.importKey("jwk", privateKeyJwk, {
    name: "RSASSA-PKCS1-v1_5",
        hash: {name: "SHA-512"},
  }, false, ['sign']);
  const data = new TextEncoder().encode(message);

  const signature = await window.crypto.subtle.sign({
      name: "RSASSA-PKCS1-v1_5",
    },
    privateKey,
    data,
  );

  // converts the signature to a colon seperated string
  return new Uint8Array(signature).join(':');
}

这个函数需要我们在第一步创建的私钥的JWK和一个简单字符串作为要签名的消息。它会返回一个用冒号分隔的字符串表示的签名。目前这个方式可以工作,即使使用Base64可能更有效率。
现在最后一步是验证你的消息。你可能想在另一台机器上进行此操作,在那里你只有公钥和可能已经损坏的消息,并且你想验证消息是否已被损坏。你绝对需要确信私钥没有被损坏。
async function verify(publicKeyJwk, signatureStr, message) {
    const signatureArr = signatureStr.split(':').map(x => +x);
  const signature = new Uint8Array(signatureArr).buffer

  const publicKey = await window.crypto.subtle.importKey("jwk", publicKeyJwk, {
    name: "RSASSA-PKCS1-v1_5",
        hash: {name: "SHA-512"},
  }, false, ['verify']);
  const data = new TextEncoder().encode(message);

  const ok = await window.crypto.subtle.verify({
      name: "RSASSA-PKCS1-v1_5",
    },
    publicKey,
    signature,
    data
  );
  return ok;
}

为此,您需要第一步创建的公钥的jwk、第二步使用的消息作为字符串以及第二步创建的签名作为冒号分隔的字符串。

这将导致一个布尔值,指示消息是否有效。


非常感谢您抽出时间尝试回答我的问题。非常感激。我必须承认,我希望有一种更简单的方法 - 就像哈希一样。今天大多数物理签名都是通过识别手写来进行身份验证的 - 这不是最安全的方式,但类似于哈希。难道没有私有和公共的哈希技术/包吗? - Norfeldt
哈希是根据定义不是公钥/私钥过程。哈希是一种将值转换为较短值的算法。例如,取模是一种有效的哈希算法。而且 3%3=06%3=0。如果您有一个公钥/私钥,您需要创建和管理它的复杂性。您到底希望什么更简单,您的具体用例是什么?也许我们在这里有一个 X/Y 问题,请谈谈您对此的具体用例。 - Lux

3
尝试使用Node模块XML高级电子签名。它使用Web Crypto进行加密操作,因此可在浏览器和Node.js中使用。

根据您的用例,您可能还希望考虑直接使用PGP,例如在节点中:https://github.com/openpgpjs/openpgpjs - rmharrison
谢谢您的回答,但我正在寻找更简单的东西。 - Norfeldt

2
这在技术上看起来像是一个合适的“数字签名”,例如使用私钥进行签名并使用公钥验证签名。 “合格”的签名需要由基本上是受信任的证书颁发机构的合格提供商发出。用于放置合格签名的密钥必须在安全设备(智能卡和HSM)上发行,以便除所有者之外的任何人都无法访问私钥。

但是高级签名和合格签名之间的法律区别并不完全清楚-条例明确规定非合格签名也具有法律价值。在浏览器中使用智能卡进行合格签名是可怕的用户体验-在大多数情况下,它通过Java Applet进行,现在基本上只在Internet Explorer和Firefox的特殊版本上工作。替代方案包括处理签名的桌面软件和本地服务JWS应用程序,但智能卡是一个重要问题,目前不是重点。

那么,我们如何允许用户“放置”电子签名?我有一个想法,可以完全使用WebCrypto API来完成,这在现今的浏览器中几乎得到支持。想法如下:

让用户输入密码以进行签名。 使用密码生成密钥(例如使用PBKDF2)。 用派生的密钥对用户提交的表单内容进行签名。 将签名与其余表单数据一起存储。 可选地,为验证目的存储派生密钥。 以下是一个JavaScript代码片段,实现了这个流程。
其中许多部分来自非常有用的webcrypto示例仓库。hex2buf、buf2hex和str2ab函数是实用程序(在JS中不是标准的)。
代码的功能很简单,尽管有点冗长。所有操作都使用Promise和“then”链接,说实话,这有点乏味,但不可避免:Class 2 Digital SignatureClass 3 Digital Signature 密码作为原始密钥进行加载(转换为数组缓冲区后)。 使用PBKDF2(100次迭代)派生出一个秘密密钥。 秘密密钥用于对用户填写的内容进行HMAC“签名”。 签名和密钥被存储(在此示例中存储在UI中)。 然后,可以使用数据、签名和密钥来验证签名。

我们开发了一个产品,使用WebCrypto API和PKIjs在浏览器中进行PAdES签名(和验证)。您可以通过此链接查看验证。https://manage.hancock.ink/document?url=https://peculiarventures.github.io/ExamplePDFs/signed/signed_example_diploma.pdf。我们还通过创建此中间件来支持智能卡,该中间件充当操作系统和Web之间的桥梁。https://fortifyapp.com - rmhrisk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接