Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(schnorr): add support for schnorr signatures #189

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions lib/elliptic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 32 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -331,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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This will allow an odd-Y DER pubkey to verify true against a schnorr signature when technically xonly pubkeys are always assumed to be even. Currently this is achieved by throwing away the parity bit in the code.

While this is pretty understandable for signing (the C library will automatically negate the private key if Y is odd) verification should be stricter IMO.

I am asking the library "Does this pubkey verify the signature X" and if the Y is odd, we're flipping the pubkey to a different pubkey before returning true... we would need to widen the definition of what "pubkey" means in the context of this function alone to mean "Either the given pubkey or its inverse"

To reduce confusion on this point, I think only the xonly (32 length) pubkey should be accepted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we could check the DER pubkeys for evenness and return false/throw. (check first byte is 0x02 for 33 length, check that pubkey[64] & 1 === 0 for 65 length)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense - test vector 3 requires negation of the seckey therefore I assumed it was fine.
https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.py#L73-L85

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That outputs a 32 byte xonly pubkey hex to the csv.
Like I said, automatically flipping to even when signing is fine. It's during verification that you need to be more strict.


switch (secp256k1.schnorrVerify(sig, msg32, pubkey)) {
case 0:
return true
case 2:
return false
case 1:
throw new Error(errors.PUBKEY_PARSE)
}
}
}
}
2 changes: 1 addition & 1 deletion src/secp256k1
Submodule secp256k1 updated 117 files
54 changes: 54 additions & 0 deletions src/secp256k1.cc
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include <secp256k1.h>
#include <secp256k1/include/secp256k1_ecdh.h>
#include <secp256k1/include/secp256k1_extrakeys.h>
#include <secp256k1/include/secp256k1_preallocated.h>
#include <secp256k1/include/secp256k1_recovery.h>
#include <secp256k1/include/secp256k1_schnorrsig.h>

// Local helpers
#define RETURN(result) return Napi::Number::New(info.Env(), result)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<Napi::Object>();
auto output_sig = output.Get("signature").As<Napi::Buffer<unsigned char>>().Data();
auto msg32 = info[1].As<Napi::Buffer<unsigned char>>().Data();
auto seckey = info[2].As<Napi::Buffer<const unsigned char>>().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<Napi::Buffer<unsigned char>>().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<Napi::Buffer<const unsigned char>>().Data();
auto msg32 = info[1].As<Napi::Buffer<const unsigned char>>();
auto input = info[2].As<Napi::Buffer<const unsigned char>>();

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);
}
3 changes: 3 additions & 0 deletions src/secp256k1.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class Secp256k1Addon : public Napi::ObjectWrap<Secp256k1Addon> {
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
2 changes: 1 addition & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
}
Expand Down
57 changes: 35 additions & 22 deletions test/privatekey.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
119 changes: 119 additions & 0 deletions test/schnorr.js
Original file line number Diff line number Diff line change
@@ -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()
})
}