Создание подписи ECDSA с помощью Node.js/crypto

У меня есть код, который генерирует конкатенированную (rs) подпись для подписи ECDSA, используя jsrsasign и ключ в формате JWK:

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-байтовые буферы; и они выполняют перекрестную проверку (т. е. я могу проверить jsrsasign подписи с SubtleCrypto и наоборот). Однако, когда я использую класс Sign в модуле Node.js crypto, я получаю совсем другое.

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

Здесь я получаю буфер переменной длины, примерно 70 байт; он не выполняет перекрестную проверку с jsrsa (что исключает жалобу на недопустимую длину подписи rs).

Как я могу получить подпись r-s, сгенерированную jsrsasign и SubtleCrypto, используя узел crypto?


person Petter Häggholm    schedule 14.09.2016    source источник
comment
Вы уверены, что require('jwk-to-pem')(key, {'private': true}); создает действительную кодировку закрытого ключа EC, которую понимает криптомодуль? Начинается ли он с ----- BEGIN EC PRIVATE KEY ------, как в примере?   -  person Artjom B.    schedule 14.09.2016
comment
Да: по крайней мере, он генерирует строку PEM с разделом EC PRIVATE KEY, а модуль crypto не выдает никаких ошибок или предупреждений, он просто выдает подпись, отличную от той, которую я ожидал.   -  person Petter Häggholm    schedule 15.09.2016


Ответы (1)


Ответ заключается в том, что модуль Node crypto генерирует подписи ASN.1/DER, в то время как другие API, такие как jsrsasign и SubtleCrypto, создают «конкатенированную» подпись. В обоих случаях подпись представляет собой конкатенацию (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'));
}

Разобрался благодаря

person Petter Häggholm    schedule 23.09.2016