【问题标题】:NodeJS Crypto Fails to Verify Signature Created by Web Crypto APINodeJS Crypto 无法验证由 Web Crypto API 创建的签名
【发布时间】:2021-01-21 20:29:14
【问题描述】:

我在验证由 Web Crypto API 创建的签名时遇到问题。

这是我用来在浏览器中生成 RSA 密钥的代码:

let keys;

const generateKeys = async () => {
  const options = {
    name: 'RSASSA-PKCS1-v1_5',
    modulusLength: 2048, 
    publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
    hash: { name: 'SHA-256' }, 
  };

  keys = await window.crypto.subtle.generateKey(
    options,
    false, // non-exportable (public key still exportable)
    ['sign', 'verify'],
  );
};

并导出公钥:

const exportPublicKey = async () => {    
  const publicKey = await window.crypto.subtle.exportKey('spki', keys.publicKey);

  let body = window.btoa(String.fromCharCode(...new Uint8Array(publicKey)));
  body = body.match(/.{1,64}/g).join('\n');

  return `-----BEGIN PUBLIC KEY-----\n${body}\n-----END PUBLIC KEY-----`;

  // Output:
  //
  // -----BEGIN PUBLIC KEY-----
  // MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx7J3SUG4sq/HSGIaGZWY
  // 8b26cfEpVFYHoDUDUORIJzA/fLE9aj+uOKpGUTSfW69rMm7DAOLDz05KaEJJSI5+
  // YbDPr2S82A2ByHHQt+Vu168sGz4noXTTSX2HIdVutaR/IJ0a5pNOa1vRR4MUW/ZO
  // YaRir3yC5YXgcFLwwQaifNZ3lZ7WndbYEjTGOcieQQ81IUP2221PZCJI52S95nYm
  // VfslsLiPhOFH7XhGSqelGYDi0cKyl0p6dKvYxFswfKKLTuWnu2BEFLjVq4S5Y9Ob
  // SGm0KL/8g7pAqjac2sMzzhHtxZ+7k8tynzAf4slJJhHMm5U4DcSelTe5zOkprCJg
  // muyv0H1Acb3tfXsBwfURjiE0cvSMhfum5I5epF+f139tsr1zNF24F2WgvEZZbXcG
  // g1LveGCJ/0BY0pzE71DU2SYiUhl+HGDv2u32vJO80jCDf2lu7izEt544a+XE+2X0
  // zVpwjNQGa2Nd4ApGosa1fbcS5MsEdbyrjMf80SAmOeb9g3y5Zt2MY7M0Njxbvmmd
  // mF20PkklpH0L01lhg2AGma4o4ojolYHzDoM5a531xTw1fZIdgbSTowz0SlAHAKD3
  // c2KCCsKlBbFcqy4q7yNX63SqmI3sNA3kTH9CQJdBloRvV103Le9C0iY8CAWQmow5
  // N/sDJUabgOMqe9yopSjb7LUCAwEAAQ==
  // -----END PUBLIC KEY-----
};

要签署消息:

const generateHash = async (message) => {
  const encoder = new TextEncoder();
  const buffer = encoder.encode(message);

  const digest = await window.crypto.subtle.digest('SHA-256', buffer);
  return digest;
};

const signMessage = async (message) => {
  const { privateKey } = keys;
  const digest = await generateHash(message);
  const signature = await window.crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, digest);
  return signature;
};

在浏览器中验证消息:

const verifyMessage = async (signature, message) => {
  const { publicKey } = keys;
  const digest = await generateHash(message);
  const result = await window.crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signature, digest);
  return result;
};

创建密钥后,将导出公钥并将其发送到服务器。稍后:

const message = 'test';
const signature = await signMessage(message);
await verifyMessage(signature, message); // true

sendToServer(message, bufferToHex(signature));

由于签名是一个ArrayBuffer,我将其转换为十六进制,代码如下:

const bufferToHex = input => [...new Uint8Array(input)]
  .map(v => v.toString(16).padStart(2, '0')).join('');

在服务器上(NodeJS 8.11.0):

const publicKey = getPublicKey(userId);

const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(message, 'utf-8');

const sigBuf = Buffer.from(signature, 'hex');
verifier.verify(publicKey, sigBuf); // false

我几天来一直在追查这个问题,但似乎无法弄清楚。我已经尝试了RSA-SHA256sha256WithRSAEncryption 进行验证,但无济于事。此外,不会引发任何错误。任何帮助将不胜感激!

【问题讨论】:

    标签: node.js cryptography rsa webcrypto-api


    【解决方案1】:

    所以我不完全理解为什么会这样,但为了解决这个问题,我需要将 SHA 哈希从 ArrayBuffer 转换为十六进制字符串,然后使用 TextEncoder 读回数组缓冲区。

    const generateHash = async (message) => {
      const encoder = new TextEncoder();
      const buffer = encoder.encode(message);
    
      const digest = await window.crypto.subtle.digest('SHA-256', buffer);
    
      // Convert to hex string
      return [...new Uint8Array(digest)]
        .map(v => v.toString(16).padStart(2, '0')).join('');;
    };
    

    然后在签名时:

    const signMessage = async (message) => {
      const encoder = new TextEncoder();
      const { privateKey } = keys;
      const digest = await generateHash(message);
      const signature = await window.crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, encoder.encode(digest));
      return signature;
    };
    

    签名不再在客户端验证,而是在节点验证。 ?‍♂️

    【讨论】:

    • 我不同意这是正确的答案。散列是多余的,因为散列是在签名算法内部完成的。
    【解决方案2】:

    问题在于,当您实际上应该对输入的哈希进行签名时,您正在对输入的哈希进行签名。 SubtleCrypto 在内部对输入进行哈希处理。您无需提供散列输入。由于您提供了散列输入作为参数,SubtleCrypto 再次对其进行散列,然后对其进行签名,这导致签名不匹配。

    【讨论】:

    • 非常有见地!谢谢!
    【解决方案3】:

    注意cryptocrypto.subtle 在签名前对消息进行哈希处理可能很有用,因此不需要单独进行哈希处理。

    假设您能够正确创建和加载密钥,您的代码应该如下所示。

    浏览器

    const message = 'hello'
    const encoder = new TextEncoder()
    const algorithmParameters = { name: 'RSASSA-PKCS1-v1_5' }
    const signatureBytes = await window.crypto.subtle.sign(
      algorithmParameters,
      privateKey,
      encoder.encode(message)
    )
    const base64Signature = window.btoa(
      String.fromCharCode.apply(null, new Uint8Array(signatureBytes))
    )
    console.log(base64Signature)
    // TiJZTTihhUYAIlOm2PpnvJa/+15WOX2U0iKJ2LXsLecvohhRIWnwFfdHy4ci10mcv/UQgf2+bFf9lfFZUlPPdzckBNfXIqAjafM8XquJiw/t1v+pEGtJpaGASlzuWuL37gp3k8ux3l6zBKKbBVPPASkHVhz37uY1AXeMblfRbFE=
    

    节点

    此实现使用crypto,但您可以使用crypto.subtle 更类似于浏览器的javascript语法

    const crypto = require('crypto')
    
    const message = 'hello'
    const base64Signature = 'TiJZTTihhUYAIlOm2PpnvJa/+15WOX2U0iKJ2LXsLecvohhRIWnwFfdHy4ci10mcv/UQgf2+bFf9lfFZUlPPdzckBNfXIqAjafM8XquJiw/t1v+pEGtJpaGASlzuWuL37gp3k8ux3l6zBKKbBVPPASkHVhz37uY1AXeMblfRbFE='
    const hashingAlgorithm = 'rsa-sha256'
    const doesVerify = crypto.verify(
      hashingAlgorithm,
      Buffer.from(message),
      { key: publicKey },
      Buffer.from(base64Signature, 'base64')
    );
    console.log(doesVerify)
    // true
    

    【讨论】:

      【解决方案4】:

      不是直接的答案,但使用它可能更容易:https://www.npmjs.com/package/@peculiar/webcrypto,因此您在客户端和服务器上的代码是一致的,同时解决了这个问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-20
        • 2014-01-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多