【问题标题】:ECDSA signatures between Node.js and WebCrypto appear to be incompatible?Node.js 和 WebCrypto 之间的 ECDSA 签名似乎不兼容?
【发布时间】:2016-09-18 04:39:02
【问题描述】:

我在 Node.js 中使用以下示例进行签名和验证:https://github.com/nodejs/node-v0.x-archive/issues/6904。验证在 Node.js 中成功,但在 WebCrypto 中失败。同样,使用 WebCrypto 签名的消息无法在 Node.js 中验证。

这是我用来验证使用 WebCrypto 的 Node.js 脚本生成的签名的代码 - https://jsfiddle.net/aj49e8sj/。在 Chrome 54.0.2840.27 和 Firefox 48.0.2 中测试

// From https://github.com/nodejs/node-v0.x-archive/issues/6904
var keys = {
  priv: '-----BEGIN EC PRIVATE KEY-----\n' +
        'MHcCAQEEIF+jnWY1D5kbVYDNvxxo/Y+ku2uJPDwS0r/VuPZQrjjVoAoGCCqGSM49\n' +
        'AwEHoUQDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNhB8i3mXyIMq704m2m52FdfKZ2\n' +
        'pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' +
        '-----END EC PRIVATE KEY-----\n',
  pub: '-----BEGIN PUBLIC KEY-----\n' +
       'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNh\n' +
       'B8i3mXyIMq704m2m52FdfKZ2pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' +
       '-----END PUBLIC KEY-----\n'
};
var message = (new TextEncoder('UTF-8')).encode('hello');

// Algorithm used in Node.js script is ecdsa-with-SHA1, key generated with prime256v1
var algorithm = {
    name: 'ECDSA',
    namedCurve: 'P-256',
    hash: {
        name: 'SHA-1'
    }
};

// Signature from obtained via above Node.js script
var sig64 = 'MEUCIQDkAtiomagyHFi7dNfxMrzx/U0Gk/ZhmwCqaL3TimvlswIgPgeDqgZNqfR5/FZZASYsczUAhGSXjuycLhWnvk20qKc=';

// Decode base64 string into ArrayBuffer
var b64Decode = (str) => Uint8Array.from(atob(str), x => x.charCodeAt(0));

// Get base64 string from public key
const key64 = keys.pub.split('\n')
    .filter(x => x.length > 0 && !x.startsWith('-----'))
    .join('');

// Convert to buffers
var sig = b64Decode(sig64);
var keySpki = b64Decode(key64);

// Import and verify
// Want 'Verification result: true' but will get 'false'
var importKey = crypto.subtle.importKey('spki', keySpki, algorithm, true, ['verify'])
    .then(key => crypto.subtle.verify(algorithm, key, sig, message))
    .then(result => console.log('Verification result: ' + result));

使用 SHA-256 而不是 SHA-1 的类似问题的相关问题:Generating ECDSA signature with Node.js/crypto

我检查过的内容:

  • 我解码了 Node.js 密钥并验证它们与通过 WebCrypto 生成的密钥具有相同的 OID。这告诉我我使用了正确的曲线。
  • SHA-1 被明确标识为要在两个位置使用的哈希。
  • 在 Node.js 和 WebCrypto 中都明确标识了 ECDSA。

如何成功验证从 Node.js 接收到的签名,反之亦然 - 验证从 WebCrypto 生成的 Node.js 中的签名?或者标准的实现是否存在细微差别,导致它们不兼容?

编辑:

  • WebCrypto 签名(64 字节):uTaUWTfF+AjN3aPj0b5Z2d1HybUEpV/phv/P9RtfKaGXtcYnbgfO43IRg46rznG3/WnWwJ2sV6mPOEnEPR0vWw==
  • Node.js 签名(71 字节):MEUCIQDkAtiomagyHFi7dNfxMrzx/U0Gk/ZhmwCqaL3TimvlswIgPgeDqgZNqfR5/FZZASYSczUAhGSXjuycLhWnvk20qKc=

经过验证的 Node.js 签名是 DER 编码的,而 WebCrypto 签名不是。

【问题讨论】:

    标签: node.js cryptography ecdsa webcrypto-api


    【解决方案1】:

    经过几个小时终于找到了一个零依赖的解决方案!

    在浏览器中:

          // Tip: Copy & Paste in the console for test.
    
          // Text to sign:
          var source = 'test';
    
          // Auxs
          function length(hex) {
            return ('00' + (hex.length / 2).toString(16)).slice(-2).toString();
          }
    
          function pubKeyToPEM(key) {
            var pem = '-----BEGIN PUBLIC KEY-----\n',
                keydata = '',
                bytes = new Uint8Array( key );
    
            for (var i = 0; i < bytes.byteLength; i++) {
              keydata += String.fromCharCode( bytes[ i ] );
            }
    
            keydata = window.btoa(keydata);
    
            while(keydata.length > 0) {
              pem += keydata.substring(0, 64) + '\n';
              keydata = keydata.substring(64);
            }
    
            pem = pem + "-----END PUBLIC KEY-----";
    
            return pem;
          }
    
          // Generate new keypair.
          window.crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["sign", "verify"])
                .then(function(keypair) {
    
                  // Encode as UTF-8
                  var enc = new TextEncoder('UTF-8'),
                      digest = enc.encode(source);
                  
                  // Sign with subtle
                  window.crypto.subtle.sign({ name: "ECDSA", hash: {name: "SHA-1"} }, keypair.privateKey, digest)
                        .then(function(signature) {
                            signature = new Uint8Array(signature);
    
                            // Extract r & s and format it in ASN1 format.
                            var signHex = Array.prototype.map.call(signature, function(x) { return ('00' + x.toString(16)).slice(-2); }).join(''),
                                r = signHex.substring(0, 96),
                                s = signHex.substring(96),
                                rPre = true,
                                sPre = true;
    
                            while(r.indexOf('00') === 0) {
                              r = r.substring(2);
                              rPre = false;
                            }
    
                            if (rPre && parseInt(r.substring(0, 2), 16) > 127) {
                              r = '00' + r;
                            }
    
                            while(s.indexOf('00') === 0) {
                              s = s.substring(2);
                              sPre = false;
                            }
    
                            if(sPre && parseInt(s.substring(0, 2), 16) > 127) {
                              s = '00' + s;
                            }
    
                            var payload = '02' + length(r) + r +
                                          '02' + length(s) + s,
                                der = '30' + length(payload) + payload;
                            
                            // Export public key un PEM format (needed by node)
                            window.crypto.subtle.exportKey('spki', keypair.publicKey)
                                                       .then(function(key) {
                                                          var pubKey = pubKeyToPEM(key);
                                                          
                                                          console.log('This is pubKey -> ', pubKey);
                                                          console.log('This is signature -> ', der);                                                      
                                                       });
                                             
                            
                            // For test, we verify the signature, nothing, anecdotal.
                            window.crypto.subtle.verify({ name: "ECDSA", hash: {name: "SHA-1"} }, keypair.publicKey, signature, digest)
                                  .then(console.log);
                        });
                        
                });

    在节点中:

    const crypto = require('crypto');
    
    // ----------------------------------------------------------------------------
    
    // Paste from browser!
    
    var puKeyPem = '-----BEGIN PUBLIC KEY-----\n' +
                   'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmDubwJuORpMMoMnvv59W8tU8PxPChh75\n' +
                   'vjlfVB2+tPY5KDy1I0ohz2US+2K1T/ROcDCSRAjyONRzzwVBm9S6bqbk3KuaT2KG\n' +
                   'ikoe0KLfTeQtdEUyq8J0aEOKRXoCJLZq\n' +
                   '-----END PUBLIC KEY-----';
                   
    var hexSign = '306402305df22aa5f4e7200b7c264c891cd3a8c5b4622c25872020832d5bb3d251773592020249a46a8349754dc58c47c4cbb7c9023053b929a98f5c8cccf2c1a4746d82fc751e044b1f76dffdf9ef73f73bee1499c5e20aadddda41e3373760b8b0f3c1bbb2';
    
    // ----------------------------------------------------------------------------
    
    var verifier = crypto.createVerify('sha1'),
        digest   = 'test';
        
    verifier.update(digest);
    verifier.end();
    
    console.log(verifier.verify(puKeyPem, hexSign, 'hex'));
    
    // ----------------------------------------------------------------------------

    【讨论】:

    • 代码可以工作,但是,您可能需要将r = signHex.substring(0, 96) s = signHex.substring(96) 的固定96 大小更改为动态signature.length/2
    • 要使这项工作适用于任何长度的签名,96 确实需要像@GramThanos 所说的那样动态化,但需要使用的是signHex.length/2,而不是原始签名长度的一半。
    【解决方案2】:

    我不能肯定地说我没有使用过这些库中的任何一个,但一种可能性是它们没有使用相同的编码类型作为签名。对于 DSA/ECDSA,有两种主要格式,IEEE P1363(Windows 使用)和 DER(OpenSSL 使用)。

    “Windows”格式具有预设大小(由 DSA 的 Q 和 ECDSA 的 P 确定(Windows 不支持 Char-2,但如果支持 Char-2 ECDSA 可能是 M) )。然后rs 都用0 向左填充,直到它们达到那个长度。

    r = 0x305s = 0x810522 的合法示例中 sizeof(Q) 为 3 个字节:

    // r
    000305
    // s
    810522
    

    对于“OpenSSL”格式,它在 DER 规则下被编码为 SEQUENCE(INTEGER(r), INTEGER(s)),看起来像

    // SEQUENCE
    30
      // (length of payload)
      0A
      // INTEGER(r)
      02
        // (length of payload)
        02
        // note the leading 0x00 is omitted
        0305
      // INTEGER(s)
      02
        // (length of payload)
        04
        // Since INTEGER is a signed type, but this represented a positive number,
        // a 0x00 has to be inserted to keep the sign bit clear.
        00810522
    

    或者,简洁地:

    • Windows:000305810522
    • OpenSSL:300A02020305020400810522

    “Windows”格式总是偶数,总是相同的长度。 “OpenSSL”格式通常大约大 6 个字节,但在中间可以增加或减少一个字节;所以有时是偶数,有时是奇数。

    Base64 解码您的 sig64 值表明它正在使用 DER 编码。使用 WebCrypto 生成几个签名;如果有任何不以0x30 开头,那么您有 IEEE/DER 问题。

    【讨论】:

    【解决方案3】:

    现在您可以生成兼容的 (nodejs 与 webcrypto) 密钥和签名,而无需修改它们。以下示例是针对 RSA 的,但 ECDSA 应该非常相似——本质在类型/格式/编码中。

    生成密钥对(nodejs):

    const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
      modulusLength: 2048,
      publicKeyEncoding: {
        type: 'spki',
        format: 'der'
      },
      privateKeyEncoding: {
        type: 'pkcs8',
        format: 'der'
      }
    });
        
    console.log('PRIVATE', Buffer.from(privateKey).toString('base64'));
    console.log('PUBLIC', Buffer.from(publicKey).toString('base64'));
    

    签署消息(nodejs):

    const signature = crypto.sign(
      'sha256',
      Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8'),
      {
        key: crypto.createPrivateKey({
          key: Buffer.from('...base64 encoded private key', 'base64'),
          format: 'der',
          type: 'pkcs8'
        }),
        padding: crypto.constants.RSA_PKCS1_PADDING,
        dsaEncoding: 'ieee-p1363'
      }
    );
    
    console.log('SIGNATURE', signature.toString('base64'));
    

    验证消息(webcrypto)- 你必须更改 vanilla js 中的 Buffer 函数

      (async () => console.log(await crypto.subtle.verify(
        { name: 'RSASSA-PKCS1-v1_5' },
        await crypto.subtle.importKey(
          'spki',
          Buffer.from('...base64 encoded public key', 'base64'),
          { name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'} },
          false,
          [ 'verify' ]
        ),
        Buffer.from('...base64 encoded signature', 'base64'),
        Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8')
      )))();
    

    【讨论】:

      猜你喜欢
      • 2012-01-03
      • 1970-01-01
      • 1970-01-01
      • 2019-10-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-14
      相关资源
      最近更新 更多