使用Node.js/crypto生成ECDSA签名

13

我有一个使用jsrsasign和JWK格式密钥生成ECDSA签名的代码,生成一个连接(r-s)签名:

const sig = new Signature({ alg: 'SHA256withECDSA' });
sig.init(KEYUTIL.getKey(key));
sig.updateHex(dataBuffer.toString('hex'));
const asn1hexSig = sig.sign();
const concatSig = ECDSA.asn1SigToConcatSig(asn1hexSig);
return new Buffer(concatSig, 'hex');

看起来可以正常工作。我还有一些使用SubtleCrypto实现相同功能的代码:

importEcdsaKey(key, 'sign') // importKey JWK -> raw
.then((privateKey) => subtle.sign(
    { name: 'ECDSA', hash: {name: 'SHA-256'} },
    privateKey,
    dataBuffer
))

这两种方法都会返回128字节的缓冲区,并且它们会进行交叉验证(即我可以使用SubtleCrypto验证jsrsasign的签名,反之亦然)。但是,当我在Node.js crypto模块中使用Sign类时,似乎得到的结果与这些有很大不同。

key = require('jwk-to-pem')(key, {'private': true});
const sign = require('crypto').createSign('sha256');
sign.update(dataBuffer);
return sign.sign(key);

在这里,我获取了一个可变长度的缓冲区,大约70个字节;它与 jsrsa 没有进行交叉验证(后者因 r-s 签名的无效长度而抛出错误)。

我如何使用 Node 的 crypto 获取由 jsrsasignSubtleCrypto 生成的 r-s 签名?


你确定 require('jwk-to-pem')(key, {'private': true}); 生成的是加密模块可以理解的 EC 私钥的有效编码吗?它是否以 ----- BEGIN EC PRIVATE KEY ------ 开头,就像示例中一样? - Artjom B.
是的,至少它生成了一个带有“EC PRIVATE KEY”部分的PEM字符串,而且“crypto”模块没有发出任何错误或警告,只是发出了与我预期不同的签名。 - Petter Häggholm
2个回答

20
答案是:答案表明Node.js的crypto模块生成ASN.1 / DER签名,而其他API(如jsrsasignSubtleCrypto)则生成“串联”签名。在两种情况下,签名都是(r, s)的串联形式。区别在于ASN.1用最少的字节进行拼接,并附加一些负载长度数据;而P1363格式使用两个32位十六进制编码整数,并在必要时用零填充它们。
以下解决方案假定“规范”格式是由SubtleCrypto使用的串联样式。
const asn1 = require('asn1.js');
const BN = require('bn.js');
const crypto = require('crypto');

const EcdsaDerSig = asn1.define('ECPrivateKey', function() {
    return this.seq().obj(
        this.key('r').int(),
        this.key('s').int()
    );
});

function asn1SigSigToConcatSig(asn1SigBuffer) {
    const rsSig = EcdsaDerSig.decode(asn1SigBuffer, 'der');
    return Buffer.concat([
        rsSig.r.toArrayLike(Buffer, 'be', 32),
        rsSig.s.toArrayLike(Buffer, 'be', 32)
    ]);
}

function concatSigToAsn1SigSig(concatSigBuffer) {
    const r = new BN(concatSigBuffer.slice(0, 32).toString('hex'), 16, 'be');
    const s = new BN(concatSigBuffer.slice(32).toString('hex'), 16, 'be');
    return EcdsaDerSig.encode({r, s}, 'der');
}

function ecdsaSign(hashBuffer, key) {
    const sign = crypto.createSign('sha256');
    sign.update(asBuffer(hashBuffer));
    const asn1SigBuffer = sign.sign(key, 'buffer');
    return asn1SigSigToConcatSig(asn1SigBuffer);
}

function ecdsaVerify(data, signature, key) {
    const verify = crypto.createVerify('SHA256');
    verify.update(data);
    const asn1sig = concatSigToAsn1Sig(signature);
    return verify.verify(key, new Buffer(asn1sig, 'hex'));
}

在以下链接的帮助下解决了问题:


我们如何在这里获取恢复参数或v值? - Yao

0
现在可以使用节点crypto包直接生成ieee-p1363格式的签名。
签名
  const payload: string = "hello";

  const signature = crypto
    .createSign("SHA256")
    .update(payload)
    .sign({
      key: privateKey,
      dsaEncoding: 'ieee-p1363',
    })
    .toString('base64');

验证
  const verified = crypto
    .createVerify("SHA256")
    .update(payload)
    .verify(
      {
        key: publicKey,
        dsaEncoding: 'ieee-p1363',
      },
      signature,
      'base64',
    );

在生产中使用此功能的示例可以在simple-jwt-auth包中找到


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