From c89e97911c186ec1f050e757dd8d4ed734b59e6e Mon Sep 17 00:00:00 2001 From: kewde Date: Wed, 16 Feb 2022 15:04:13 +0100 Subject: [PATCH 1/2] bump: libsecp256k1 to version that supports schnorr signatures --- lib/elliptic.js | 14 +++++++++--- lib/index.js | 3 +-- src/secp256k1 | 2 +- test/privatekey.js | 57 ++++++++++++++++++++++++++++------------------ 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/lib/elliptic.js b/lib/elliptic.js index bd48ec1..46549f3 100644 --- a/lib/elliptic.js +++ b/lib/elliptic.js @@ -66,21 +66,29 @@ function savePublicKey (output, point) { for (let i = 0; i < output.length; ++i) output[i] = pubkey[i] } +function privateKeyVerify (seckey) { + const bn = new BN(seckey) + // TODO: add check for overflow (and maybe underflow too?) + const isOverflow = false + const isNotN = bn.cmp(ecparams.n) < 0 + const isNotZero = !bn.isZero() + return !isOverflow && isNotN && isNotZero ? 0 : 1 +} + module.exports = { contextRandomize () { return 0 }, privateKeyVerify (seckey) { - const bn = new BN(seckey) - return bn.cmp(ecparams.n) < 0 && !bn.isZero() ? 0 : 1 + return privateKeyVerify(seckey) }, privateKeyNegate (seckey) { const bn = new BN(seckey) const negate = ecparams.n.sub(bn).umod(ecparams.n).toArrayLike(Uint8Array, 'be', 32) seckey.set(negate) - return 0 + return privateKeyVerify(negate) }, privateKeyTweakAdd (seckey, tweak) { diff --git a/lib/index.js b/lib/index.js index 8187e17..da42969 100644 --- a/lib/index.js +++ b/lib/index.js @@ -75,14 +75,13 @@ module.exports = (secp256k1) => { case 0: return seckey case 1: - throw new Error(errors.IMPOSSIBLE_CASE) + throw new Error(errors.SECKEY_INVALID) } }, privateKeyTweakAdd (seckey, tweak) { isUint8Array('private key', seckey, 32) isUint8Array('tweak', tweak, 32) - switch (secp256k1.privateKeyTweakAdd(seckey, tweak)) { case 0: return seckey diff --git a/src/secp256k1 b/src/secp256k1 index d644dda..9f56bdf 160000 --- a/src/secp256k1 +++ b/src/secp256k1 @@ -1 +1 @@ -Subproject commit d644dda5c9dbdecee52d1aa259235510fdc2d4ee +Subproject commit 9f56bdf5b9ba2e22e77c6adaaeb8302398732df3 diff --git a/test/privatekey.js b/test/privatekey.js index 01bfdfe..c05858e 100644 --- a/test/privatekey.js +++ b/test/privatekey.js @@ -41,32 +41,45 @@ module.exports = (t, secp256k1) => { t.test('privateKeyNegate', (t) => { t.test('arg: invalid private key', (t) => { - t.throws(() => { - secp256k1.privateKeyNegate(null) - }, /^Error: Expected private key to be an Uint8Array$/, 'should be an Uint8Array') - - t.throws(() => { - const privateKey = util.getPrivateKey().slice(1) - secp256k1.privateKeyNegate(privateKey) - }, /^Error: Expected private key to be an Uint8Array with length 32$/, 'should have length 32') - + const fixtures = [ + { + privateKey: null, + expected: /^Error: Expected private key to be an Uint8Array$/, + msg: 'should be an Uint8Array' + }, + { + privateKey: util.getPrivateKey().slice(1), + expected: /^Error: Expected private key to be an Uint8Array with length 32$/, + msg: 'should have length 32' + }, + { + privateKey: util.BN_ZERO.toArrayLike(Buffer, 'be', 32), + expected: /^Error: Private Key is invalid$/, + msg: 'should be invalid private key' + }, + { + privateKey: util.ec.curve.n.toArrayLike(Buffer, 'be', 32), + expected: /^Error: Private Key is invalid$/, + msg: 'should be invalid private key' + }, + { + privateKey: util.ec.curve.n.addn(10).toArrayLike(Buffer, 'be', 32), + expected: /^Error: Private Key is invalid$/, + msg: 'should be invalid private key' + } + ] + for (const { privateKey, expected, msg } of fixtures) { + t.throws(() => { + console.log(privateKey) + console.log(secp256k1.privateKeyNegate(privateKey)) + }, expected, msg) + } t.end() }) t.test('negate valid private keys', (t) => { - const fixtures = [{ - privateKey: util.BN_ZERO.toArrayLike(Buffer, 'be', 32), - expected: Buffer.allocUnsafe(32).fill(0x00), - msg: 'negate 0 private key' - }, { - privateKey: util.ec.curve.n.toArrayLike(Buffer, 'be', 32), - expected: Buffer.allocUnsafe(32).fill(0x00), - msg: 'negate N private key' - }, { - privateKey: util.ec.curve.n.addn(10).toArrayLike(Buffer, 'be', 32), - expected: util.ec.curve.n.subn(10).toArrayLike(Buffer, 'be', 32), - msg: 'negate overflowed private key' - }] + const fixtures = [ + ] for (const { privateKey, expected, msg } of fixtures) { const negated = secp256k1.privateKeyNegate(privateKey) From d7bee5a144feae266fb17bb06bea47c3c88e52e8 Mon Sep 17 00:00:00 2001 From: kewde Date: Wed, 16 Feb 2022 20:54:04 +0100 Subject: [PATCH 2/2] feat(schnorr): add support for schnorr sign and verify --- binding.gyp | 2 + lib/index.js | 31 ++++++++++++ src/secp256k1.cc | 54 +++++++++++++++++++++ src/secp256k1.h | 3 ++ test/index.js | 2 +- test/schnorr.js | 119 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 test/schnorr.js diff --git a/binding.gyp b/binding.gyp index ef13a12..de9876d 100644 --- a/binding.gyp +++ b/binding.gyp @@ -40,6 +40,8 @@ # Activate modules 'ENABLE_MODULE_ECDH=1', 'ENABLE_MODULE_RECOVERY=1', + 'ENABLE_MODULE_EXTRAKEYS=1', + 'ENABLE_MODULE_SCHNORRSIG=1', # 'USE_ENDOMORPHISM=1', # Ignore GMP, dynamic linking, so will be hard to use with prebuilds diff --git a/lib/index.js b/lib/index.js index da42969..041d05b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -330,6 +330,37 @@ module.exports = (secp256k1) => { case 2: throw new Error(errors.ECDH) } + }, + + schnorrSign (msg32, seckey, output) { + isUint8Array('message', msg32, 32) + isUint8Array('private key', seckey, 32) + output = getAssertedOutput(output, 64) + + const obj = { signature: output } + switch (secp256k1.schnorrSign(obj, msg32, seckey)) { + case 0: + return obj + case 1: + throw new Error(errors.SIGN) + case 2: + throw new Error(errors.IMPOSSIBLE_CASE) + } + }, + + schnorrVerify (sig, msg32, pubkey) { + isUint8Array('signature', sig, 64) + isUint8Array('message', msg32, 32) + isUint8Array('public key', pubkey, [32, 33, 65]) + + switch (secp256k1.schnorrVerify(sig, msg32, pubkey)) { + case 0: + return true + case 2: + return false + case 1: + throw new Error(errors.PUBKEY_PARSE) + } } } } diff --git a/src/secp256k1.cc b/src/secp256k1.cc index e97d4a6..3ffdb1b 100644 --- a/src/secp256k1.cc +++ b/src/secp256k1.cc @@ -1,7 +1,9 @@ #include #include +#include #include #include +#include // Local helpers #define RETURN(result) return Napi::Number::New(info.Env(), result) @@ -64,6 +66,9 @@ Napi::Value Secp256k1Addon::Init(Napi::Env env) { InstanceMethod("ecdsaRecover", &Secp256k1Addon::ECDSARecover), InstanceMethod("ecdh", &Secp256k1Addon::ECDH), + + InstanceMethod("schnorrSign", &Secp256k1Addon::SchnorrSign), + InstanceMethod("schnorrVerify", &Secp256k1Addon::SchnorrVerify), }); constructor = Napi::Persistent(func); @@ -413,3 +418,52 @@ Napi::Value Secp256k1Addon::ECDH(const Napi::CallbackInfo& info) { 2); RETURN(0); } + +Napi::Value Secp256k1Addon::SchnorrSign(const Napi::CallbackInfo& info) { + auto output = info[0].As(); + auto output_sig = output.Get("signature").As>().Data(); + auto msg32 = info[1].As>().Data(); + auto seckey = info[2].As>().Data(); + + secp256k1_keypair keypair; + RETURN_IF_ZERO(secp256k1_keypair_create( + this->ctx_, &keypair, seckey), + 1); + + const unsigned char* noncedata = NULL; + if (!info[3].IsUndefined()) { + noncedata = info[3].As>().Data(); + } + + RETURN_IF_ZERO(secp256k1_schnorrsig_sign( + this->ctx_, output_sig, msg32, &keypair, noncedata), + 1); + + RETURN(0); +} + +Napi::Value Secp256k1Addon::SchnorrVerify(const Napi::CallbackInfo& info) { + auto sig = info[0].As>().Data(); + auto msg32 = info[1].As>(); + auto input = info[2].As>(); + + secp256k1_xonly_pubkey pubkeyX; + if (input.Length() == 32) { + RETURN_IF_ZERO(secp256k1_xonly_pubkey_parse(this->ctx_, &pubkeyX, input.Data()), 1); + } else { + printf("else"); + secp256k1_pubkey pubkey; + RETURN_IF_ZERO(secp256k1_ec_pubkey_parse( + this->ctx_, &pubkey, input.Data(), input.Length()), + 1); + + int pk_parity; + RETURN_IF_ZERO(secp256k1_xonly_pubkey_from_pubkey( + this->ctx_, &pubkeyX, &pk_parity, &pubkey), + 1); + } + + RETURN_IF_ZERO(secp256k1_schnorrsig_verify(this->ctx_, sig, msg32.Data(), msg32.Length(), &pubkeyX), 2); + + RETURN(0); +} diff --git a/src/secp256k1.h b/src/secp256k1.h index 6dabbf3..6eaced1 100644 --- a/src/secp256k1.h +++ b/src/secp256k1.h @@ -58,6 +58,9 @@ class Secp256k1Addon : public Napi::ObjectWrap { Napi::Value ECDSARecover(const Napi::CallbackInfo& info); Napi::Value ECDH(const Napi::CallbackInfo& info); + + Napi::Value SchnorrSign(const Napi::CallbackInfo& info); + Napi::Value SchnorrVerify(const Napi::CallbackInfo& info); }; #endif // ADDON_SECP256K1 diff --git a/test/index.js b/test/index.js index 96a9913..ffe1bdb 100644 --- a/test/index.js +++ b/test/index.js @@ -11,7 +11,7 @@ function testAPI (secp256k1, description) { require('./signature')(t, secp256k1) require('./ecdsa')(t, secp256k1) require('./ecdh')(t, secp256k1) - + if (!process.browser) require('./schnorr')(t, secp256k1) t.end() }) } diff --git a/test/schnorr.js b/test/schnorr.js new file mode 100644 index 0000000..d722a40 --- /dev/null +++ b/test/schnorr.js @@ -0,0 +1,119 @@ +const util = require('./util') + +const testVectors = [{ + pk: [ + 0xD6, 0x9C, 0x35, 0x09, 0xBB, 0x99, 0xE4, 0x12, + 0xE6, 0x8B, 0x0F, 0xE8, 0x54, 0x4E, 0x72, 0x83, + 0x7D, 0xFA, 0x30, 0x74, 0x6D, 0x8B, 0xE2, 0xAA, + 0x65, 0x97, 0x5F, 0x29, 0xD2, 0x2D, 0xC7, 0xB9 + ], + msg: [ + 0x4D, 0xF3, 0xC3, 0xF6, 0x8F, 0xCC, 0x83, 0xB2, + 0x7E, 0x9D, 0x42, 0xC9, 0x04, 0x31, 0xA7, 0x24, + 0x99, 0xF1, 0x78, 0x75, 0xC8, 0x1A, 0x59, 0x9B, + 0x56, 0x6C, 0x98, 0x89, 0xB9, 0x69, 0x67, 0x03 + ], + sig: [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3B, 0x78, 0xCE, 0x56, 0x3F, + 0x89, 0xA0, 0xED, 0x94, 0x14, 0xF5, 0xAA, 0x28, + 0xAD, 0x0D, 0x96, 0xD6, 0x79, 0x5F, 0x9C, 0x63, + 0x76, 0xAF, 0xB1, 0x54, 0x8A, 0xF6, 0x03, 0xB3, + 0xEB, 0x45, 0xC9, 0xF8, 0x20, 0x7D, 0xEE, 0x10, + 0x60, 0xCB, 0x71, 0xC0, 0x4E, 0x80, 0xF5, 0x93, + 0x06, 0x0B, 0x07, 0xD2, 0x83, 0x08, 0xD7, 0xF4 + ] +} +] + +module.exports = (t, secp256k1) => { + t.test('schnorrSign', (t) => { + t.test('arg: invalid message', (t) => { + t.throws(() => { + secp256k1.schnorrSign(null) + }, /^Error: Expected message to be an Uint8Array$/, 'should be be an Uint8Array') + + t.throws(() => { + const message = util.getMessage().slice(1) + secp256k1.schnorrSign(message) + }, /^Error: Expected message to be an Uint8Array with length 32$/, 'should have length 32') + + t.end() + }) + + t.test('arg: invalid private key', (t) => { + t.throws(() => { + const message = util.getMessage() + secp256k1.schnorrSign(message, null) + }, /^Error: Expected private key to be an Uint8Array$/, 'should be be an Uint8Array') + + t.throws(() => { + const message = util.getMessage() + const privateKey = util.getPrivateKey().slice(1) + secp256k1.schnorrSign(message, privateKey) + }, /^Error: Expected private key to be an Uint8Array with length 32$/, 'should have length 32') + + t.throws(() => { + const message = util.getMessage() + const privateKey = new Uint8Array(32) + secp256k1.schnorrSign(message, privateKey) + }, /^Error: The nonce generation function failed, or the private key was invalid$/, 'should throw on zero private key') + + t.throws(() => { + const message = util.getMessage() + const privateKey = util.ec.n.toArrayLike(Buffer, 'be', 32) + secp256k1.schnorrSign(message, privateKey) + }, /^Error: The nonce generation function failed, or the private key was invalid$/, 'should throw on overflowed private key: equal to N') + + t.end() + }) + + t.test('arg: invalid output', (t) => { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + + t.throws(() => { + secp256k1.schnorrSign(message, privateKey, null) + }, /^Error: Expected output to be an Uint8Array$/, 'should be an Uint8Array') + + t.throws(() => { + secp256k1.schnorrSign(message, privateKey, new Uint8Array(42)) + }, /^Error: Expected output to be an Uint8Array with length 64$/, 'should have length 64') + + secp256k1.schnorrSign(message, privateKey, (len) => { + t.same(len, 64, 'should ask Uint8Array with length 64') + return new Uint8Array(len) + }) + + t.plan(3) + t.end() + }) + + t.test('should sign and verify', (t) => { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + + const { signature } = secp256k1.schnorrSign(message, privateKey, (len) => { + return new Uint8Array(len) + }) + + const verified = secp256k1.schnorrVerify(signature, message, publicKey) + t.same(verified, true, 'verify own signature') + t.end() + }) + + t.test('should verify testvectors', (t) => { + testVectors.forEach((tv) => { + const publicKey = Buffer.from(tv.pk) + const message = Buffer.from(tv.msg) + const signature = Buffer.from(tv.sig) + const verified = secp256k1.schnorrVerify(signature, message, publicKey) + t.same(verified, true, 'verify own signature') + }) + t.end() + }) + + t.end() + }) +}