diff --git a/package-lock.json b/package-lock.json index caf12e1..a7b421b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2476,9 +2476,9 @@ } }, "tiny-secp256k1": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.1.2.tgz", - "integrity": "sha512-8qPw7zDK6Hco2tVGYGQeOmOPp/hZnREwy2iIkcq0ygAuqc9WHo29vKN94lNymh1QbB3nthtAMF6KTIrdbsIotA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.0.tgz", + "integrity": "sha512-2hPuUGCroLrxh6xxwoe+1RgPpOOK1w2uTnhgiHBpvoutBR+krNuT4hOXQyOaaYnZgoXBB6hBYkuAJHxyeBOPzQ==", "dev": true, "requires": { "uint8array-tools": "0.0.6" diff --git a/package.json b/package.json index dc790c4..4a1d9da 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "prettier": "^2.4.1", "proxyquire": "^2.0.1", "rimraf": "^2.6.3", - "tiny-secp256k1": "^2.1.2", + "tiny-secp256k1": "^2.2.0", "ts-node": "^8.3.0", "tslint": "^6.1.3", "typescript": "^4.4.4" diff --git a/src/ecpair.d.ts b/src/ecpair.d.ts index f4093b1..be184b8 100644 --- a/src/ecpair.d.ts +++ b/src/ecpair.d.ts @@ -26,8 +26,8 @@ export interface ECPairInterface extends Signer { privateKey?: Buffer; toWIF(): string; verify(hash: Buffer, signature: Buffer): boolean; - verifySchnorr(hash: Buffer, signature: Buffer): boolean; - signSchnorr(hash: Buffer): Buffer; + verifySchnorr(hash: Buffer, signature: Buffer, tweak?: Buffer): boolean; + signSchnorr(hash: Buffer, extraEntropy?: Buffer, tweak?: Buffer): Buffer; } export interface ECPairAPI { isPoint(maybePoint: any): boolean; @@ -39,8 +39,11 @@ export interface ECPairAPI { export interface TinySecp256k1Interface { isPoint(p: Uint8Array): boolean; pointCompress(p: Uint8Array, compressed?: boolean): Uint8Array; - isPrivate(d: Uint8Array): boolean; pointFromScalar(d: Uint8Array, compressed?: boolean): Uint8Array | null; + pointAddScalar(p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null; + isPrivate(d: Uint8Array): boolean; + privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; + privateNegate(d: Uint8Array): Uint8Array; sign(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; signSchnorr?(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; verify(h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean; diff --git a/src/ecpair.js b/src/ecpair.js index 7d40f4a..2abfcde 100644 --- a/src/ecpair.js +++ b/src/ecpair.js @@ -116,19 +116,42 @@ function ECPairFactory(ecc) { return Buffer.from(sig); } } - signSchnorr(hash) { + signSchnorr(hash, e, tweak) { if (!this.privateKey) throw new Error('Missing private key'); if (!ecc.signSchnorr) throw new Error('signSchnorr not supported by ecc library'); - return Buffer.from(ecc.signSchnorr(hash, this.privateKey)); + if (!tweak) return Buffer.from(ecc.signSchnorr(hash, this.privateKey, e)); + const privateKey = + this.publicKey[0] === 2 + ? this.privateKey + : ecc.privateNegate(this.privateKey); + const tweakedPrivateKey = ecc.privateAdd(privateKey, tweak); + if (!tweakedPrivateKey) { + throw new Error('Invalid tweaked private key!'); + } + return Buffer.from(ecc.signSchnorr(hash, tweakedPrivateKey, e)); } verify(hash, signature) { return ecc.verify(hash, this.publicKey, signature); } - verifySchnorr(hash, signature) { + verifySchnorr(hash, signature, tweak) { if (!ecc.verifySchnorr) throw new Error('verifySchnorr not supported by ecc library'); - return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); + if (!tweak) + return ecc.verifySchnorr( + hash, + this.publicKey.subarray(1, 33), + signature, + ); + const tweakedPublicKey = ecc.pointAddScalar(this.publicKey, tweak); + if (!tweakedPublicKey) { + throw new Error('Invalid tweaked publc key!'); + } + return ecc.verifySchnorr( + hash, + tweakedPublicKey.subarray(1, 33), + signature, + ); } } return { diff --git a/src/testecc.js b/src/testecc.js index 94121bd..d6d6266 100644 --- a/src/testecc.js +++ b/src/testecc.js @@ -42,6 +42,61 @@ function testEcc(ecc) { h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142'), ), ); + // 1 + 0 == 1 + assert( + Buffer.from( + ecc.privateAdd( + h('0000000000000000000000000000000000000000000000000000000000000001'), + h('0000000000000000000000000000000000000000000000000000000000000000'), + ), + ).equals( + h('0000000000000000000000000000000000000000000000000000000000000001'), + ), + ); + // -3 + 3 == 0 + assert( + ecc.privateAdd( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), + h('0000000000000000000000000000000000000000000000000000000000000003'), + ) === null, + ); + assert( + Buffer.from( + ecc.privateAdd( + h('e211078564db65c3ce7704f08262b1f38f1ef412ad15b5ac2d76657a63b2c500'), + h('b51fbb69051255d1becbd683de5848242a89c229348dd72896a87ada94ae8665'), + ), + ).equals( + h('9730c2ee69edbb958d42db7460bafa18fef9d955325aec99044c81c8282b0a24'), + ), + ); + assert( + Buffer.from( + ecc.privateNegate( + h('0000000000000000000000000000000000000000000000000000000000000001'), + ), + ).equals( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ); + assert( + Buffer.from( + ecc.privateNegate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), + ), + ).equals( + h('0000000000000000000000000000000000000000000000000000000000000003'), + ), + ); + assert( + Buffer.from( + ecc.privateNegate( + h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'), + ), + ).equals( + h('4eede1bf775995d70a494f0a7bb6bc11e0b8cccd41cce8009ab1132c8b0a3792'), + ), + ); assert( Buffer.from( ecc.pointCompress( @@ -90,6 +145,26 @@ function testEcc(ecc) { ), ), ); + assert( + Buffer.from( + ecc.pointAddScalar( + h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h('0000000000000000000000000000000000000000000000000000000000000002'), + ), + ).equals( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + Buffer.from( + ecc.pointAddScalar( + h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ).equals( + h('03c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'), + ), + ); assert( Buffer.from( ecc.pointFromScalar( diff --git a/test/ecpair.spec.ts b/test/ecpair.spec.ts index 0463bb4..82b52b7 100644 --- a/test/ecpair.spec.ts +++ b/test/ecpair.spec.ts @@ -315,6 +315,26 @@ describe('ECPair', () => { ); }); + it('creates signature for tweaked private key', () => { + const kP = ECPair.fromPrivateKey(ONE, { + compressed: true, + }); + const h = Buffer.alloc(32, 2); + const tweak = Buffer.from( + '3cf5216d476a5e637bf0da674e50ddf55c403270dd36494dfcca438132fa30e7', + 'hex', + ); + const schnorrTweakedSig = Buffer.from( + 'c2cc6ba5ac926ae7a99b2dc4d410532c05787c562e5ae02fb4f204e8a4e86384ef4b436597bbc1808dcfa9f3e4223e0c2a9ab6fd4b43ab2c6d18405c43b8f0a6', + 'hex', + ); + + assert.deepStrictEqual( + kP.signSchnorr(h, undefined, tweak).toString('hex'), + schnorrTweakedSig.toString('hex'), + ); + }); + it( 'wraps tinysecp.signSchnorr', hoodwink(function (this: any): void { @@ -386,6 +406,23 @@ describe('ECPair', () => { assert.strictEqual(kP.verifySchnorr(h, schnorrsig), true); }); + it('checks signature for tweaked pubic key', () => { + const kP = ECPair.fromPrivateKey(ONE, { + compressed: false, + }); + const h = Buffer.alloc(32, 2); + const tweak = Buffer.from( + '3cf5216d476a5e637bf0da674e50ddf55c403270dd36494dfcca438132fa30e7', + 'hex', + ); + const schnorrsig = Buffer.from( + 'c2cc6ba5ac926ae7a99b2dc4d410532c05787c562e5ae02fb4f204e8a4e86384ef4b436597bbc1808dcfa9f3e4223e0c2a9ab6fd4b43ab2c6d18405c43b8f0a6', + 'hex', + ); + + assert.strictEqual(kP.verifySchnorr(h, schnorrsig, tweak), true); + }); + it( 'wraps tinysecp.verifySchnorr', hoodwink(function (this: any): void { diff --git a/ts_src/ecpair.ts b/ts_src/ecpair.ts index 25c2592..2572936 100644 --- a/ts_src/ecpair.ts +++ b/ts_src/ecpair.ts @@ -40,8 +40,8 @@ export interface ECPairInterface extends Signer { privateKey?: Buffer; toWIF(): string; verify(hash: Buffer, signature: Buffer): boolean; - verifySchnorr(hash: Buffer, signature: Buffer): boolean; - signSchnorr(hash: Buffer): Buffer; + verifySchnorr(hash: Buffer, signature: Buffer, tweak?: Buffer): boolean; + signSchnorr(hash: Buffer, extraEntropy?: Buffer, tweak?: Buffer): Buffer; } export interface ECPairAPI { @@ -55,8 +55,16 @@ export interface ECPairAPI { export interface TinySecp256k1Interface { isPoint(p: Uint8Array): boolean; pointCompress(p: Uint8Array, compressed?: boolean): Uint8Array; - isPrivate(d: Uint8Array): boolean; pointFromScalar(d: Uint8Array, compressed?: boolean): Uint8Array | null; + pointAddScalar( + p: Uint8Array, + tweak: Uint8Array, + compressed?: boolean, + ): Uint8Array | null; + + isPrivate(d: Uint8Array): boolean; + privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; + privateNegate(d: Uint8Array): Uint8Array; sign(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; signSchnorr?(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; @@ -204,21 +212,46 @@ export function ECPairFactory(ecc: TinySecp256k1Interface): ECPairAPI { } } - signSchnorr(hash: Buffer): Buffer { + signSchnorr(hash: Buffer, e?: Buffer, tweak?: Buffer): Buffer { if (!this.privateKey) throw new Error('Missing private key'); if (!ecc.signSchnorr) throw new Error('signSchnorr not supported by ecc library'); - return Buffer.from(ecc.signSchnorr(hash, this.privateKey)); + if (!tweak) return Buffer.from(ecc.signSchnorr(hash, this.privateKey, e)); + + const privateKey = + this.publicKey[0] === 2 + ? this.privateKey + : ecc.privateNegate(this.privateKey!); + + const tweakedPrivateKey = ecc.privateAdd(privateKey, tweak); + if (!tweakedPrivateKey) { + throw new Error('Invalid tweaked private key!'); + } + return Buffer.from(ecc.signSchnorr(hash, tweakedPrivateKey, e)); } verify(hash: Buffer, signature: Buffer): boolean { return ecc.verify(hash, this.publicKey, signature); } - verifySchnorr(hash: Buffer, signature: Buffer): boolean { + verifySchnorr(hash: Buffer, signature: Buffer, tweak?: Buffer): boolean { if (!ecc.verifySchnorr) throw new Error('verifySchnorr not supported by ecc library'); - return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); + if (!tweak) + return ecc.verifySchnorr( + hash, + this.publicKey.subarray(1, 33), + signature, + ); + const tweakedPublicKey = ecc.pointAddScalar(this.publicKey, tweak); + if (!tweakedPublicKey) { + throw new Error('Invalid tweaked publc key!'); + } + return ecc.verifySchnorr( + hash, + tweakedPublicKey.subarray(1, 33), + signature, + ); } } diff --git a/ts_src/testecc.ts b/ts_src/testecc.ts index 24ba9ea..7ab9afa 100644 --- a/ts_src/testecc.ts +++ b/ts_src/testecc.ts @@ -42,6 +42,65 @@ export function testEcc(ecc: TinySecp256k1Interface): void { h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142'), ), ); + // 1 + 0 == 1 + assert( + Buffer.from( + ecc.privateAdd( + h('0000000000000000000000000000000000000000000000000000000000000001'), + h('0000000000000000000000000000000000000000000000000000000000000000'), + )!, + ).equals( + h('0000000000000000000000000000000000000000000000000000000000000001'), + ), + ); + // -3 + 3 == 0 + assert( + ecc.privateAdd( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), + h('0000000000000000000000000000000000000000000000000000000000000003'), + ) === null, + ); + assert( + Buffer.from( + ecc.privateAdd( + h('e211078564db65c3ce7704f08262b1f38f1ef412ad15b5ac2d76657a63b2c500'), + h('b51fbb69051255d1becbd683de5848242a89c229348dd72896a87ada94ae8665'), + )!, + ).equals( + h('9730c2ee69edbb958d42db7460bafa18fef9d955325aec99044c81c8282b0a24'), + ), + ); + + assert( + Buffer.from( + ecc.privateNegate( + h('0000000000000000000000000000000000000000000000000000000000000001'), + )!, + ).equals( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ); + + assert( + Buffer.from( + ecc.privateNegate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), + )!, + ).equals( + h('0000000000000000000000000000000000000000000000000000000000000003'), + ), + ); + + assert( + Buffer.from( + ecc.privateNegate( + h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'), + )!, + ).equals( + h('4eede1bf775995d70a494f0a7bb6bc11e0b8cccd41cce8009ab1132c8b0a3792'), + ), + ); + assert( Buffer.from( ecc.pointCompress( @@ -90,6 +149,26 @@ export function testEcc(ecc: TinySecp256k1Interface): void { ), ), ); + assert( + Buffer.from( + ecc.pointAddScalar( + h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h('0000000000000000000000000000000000000000000000000000000000000002'), + )!, + ).equals( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + Buffer.from( + ecc.pointAddScalar( + h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + )!, + ).equals( + h('03c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'), + ), + ); assert( Buffer.from( ecc.pointFromScalar(