ECDSA signatures between Node.js and WebCrypto appear to be incompatible?

Having not used either of these libraries I can't say for certain, but one possibility is that they don't use the same encoding type for the signature. For DSA/ECDSA there are two main formats, IEEE P1363 (used by Windows) and DER (used by OpenSSL).

The "Windows" format is to have a preset size (determined by Q for DSA and P for ECDSA (Windows doesn't support Char-2, but if it did it'd probably be M for Char-2 ECDSA)). Then both r and s are left-padded with 0 until they meet that length.

In the too small to be legal example of r = 0x305 and s = 0x810522 with sizeof(Q) being 3 bytes:

// r
000305
// s
810522

For the "OpenSSL" format it is encoded under the rules of DER as SEQUENCE(INTEGER(r), INTEGER(s)), which looks like

// 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

or, compactly:

  • Windows: 000305810522
  • OpenSSL: 300A02020305020400810522

The "Windows" format is always even, always the same length. The "OpenSSL" format is usually about 6 bytes bigger, but can gain or lose a byte in the middle; so it's sometimes even, sometimes odd.

Base64-decoding your sig64 value shows that it is using the DER encoding. Generate a couple signatures with WebCrypto; if any don't start with 0x30 then you have the IEEE/DER problem.


After many hours finally find a solution with zero dependences!!

In browser:

      // 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);
                    });
                    
            });

In node:

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'));

// ----------------------------------------------------------------------------