Generating ECDSA signature with Node.js/crypto
Asked Answered
J

2

14

I have code that generates a concatenated (r-s) signature for the ECDSA signature using jsrsasign and a key in JWK format:

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

Seems to work. I also have code that uses SubtleCrypto to achieve the same thing:

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

These both return 128-byte buffers; and they cross-verify (i.e. I can verify jsrsasign signatures with SubtleCrypto and vice versa). However, when I use the Sign class in the Node.js crypto module, I seem to get something quite different.

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

Here I get a buffer of variable length, roughly 70 bytes; it does not cross-verify with jsrsa (which bails complaining about an invalid length for an r-s signature).

How can I get an r-s signature, as generated by jsrsasign and SubtleCrypto, using Node crypto?

Jimmie answered 14/9, 2016 at 20:22 Comment(2)
Are you sure that require('jwk-to-pem')(key, {'private': true}); produces a valid encoding of an EC private key that the crypto module understands? Does it begin with ----- BEGIN EC PRIVATE KEY ------ as in the example?Headfirst
Yes: at least, it does generate a PEM string with an EC PRIVATE KEY section, and the crypto module doesn’t issue any kind of errors or warnings, it just issues a signature different from what I expected.Wineglass
J
19

The answer turns out to be that the Node crypto module generates ASN.1/DER signatures, while other APIs like jsrsasign and SubtleCrypto produce a “concatenated” signature. In both cases, the signature is a concatenation of (r, s). The difference is that ASN.1 does so with the minimum number of bytes, plus some payload length data; while the P1363 format uses two 32-bit hex encoded integers, zero-padding them if necessary.

The below solution assumes that the “canonical” format is the concatenated style used by 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'));
}

Figured it out thanks to

Jimmie answered 23/9, 2016 at 2:4 Comment(1)
How can we get the recovery parameter or the v value here?Broiler
R
0

it is now possible to use the node crypto package to produce a signature in ieee-p1363 format directly

sign

  const payload: string = "hello";

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

verify

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

examples of this being used in production can be found in the simple-jwt-auth package

Randalrandall answered 23/9, 2023 at 11:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.