From c8dfd2c442ff8589a21b493412137813224394e0 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 11 Nov 2021 22:45:11 +0900 Subject: [PATCH 1/2] Add schnorr signing and verification --- src/bip32.js | 8 ++++++++ test/index.js | 6 +++++- ts-src/bip32.ts | 13 +++++++++++++ types/bip32.d.ts | 4 ++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/bip32.js b/src/bip32.js index 86f806d..deb6f09 100644 --- a/src/bip32.js +++ b/src/bip32.js @@ -209,9 +209,17 @@ function default_1(ecc) { return sig; } } + signSchnorr(hash) { + if (!this.privateKey) + throw new Error('Missing private key'); + return Buffer.from(ecc.signSchnorr(hash, this.privateKey)); + } verify(hash, signature) { return ecc.verify(hash, this.publicKey, signature); } + verifySchnorr(hash, signature) { + return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); + } } function fromBase58(inString, network) { const buffer = bs58check.decode(inString); diff --git a/test/index.js b/test/index.js index be09084..2d44965 100644 --- a/test/index.js +++ b/test/index.js @@ -215,15 +215,19 @@ tape('ecdsa', (t) => { let seed = Buffer.alloc(32, 1) let hash = Buffer.alloc(32, 2) let signature = Buffer.from('9636ee2fac31b795a308856b821ebe297dda7b28220fb46ea1fbbd7285977cc04c82b734956246a0f15a9698f03f546d8d96fe006c8e7bd2256ca7c8229e6f5c', 'hex') + let schnorrsig = Buffer.from('2fae8b517cb0e7302ca48a4109d1819e3d75af96bd58d297023e3058c4e98ff812fe6ae32a2b2bc4abab10f88f7fe56efbafc8a4e4fa437af78926f528b0585e', 'hex') let signatureLowR = Buffer.from('0587a40b391b76596c257bf59565b24eaff2cc42b45caa2640902e73fb97a6e702c3402ab89348a7dae1bf171c3e172fa60353d7b01621a94cb7caca59b995db', 'hex') let node = BIP32.fromSeed(seed) - t.plan(6) + t.plan(9) t.equal(node.sign(hash).toString('hex'), signature.toString('hex')) t.equal(node.sign(hash, true).toString('hex'), signatureLowR.toString('hex')) + t.equal(node.signSchnorr(hash).toString('hex'), schnorrsig.toString('hex')) t.equal(node.verify(hash, signature), true) t.equal(node.verify(seed, signature), false) t.equal(node.verify(hash, signatureLowR), true) t.equal(node.verify(seed, signatureLowR), false) + t.equal(node.verifySchnorr(hash, schnorrsig), true) + t.equal(node.verifySchnorr(seed, schnorrsig), false) }) }) \ No newline at end of file diff --git a/ts-src/bip32.ts b/ts-src/bip32.ts index 0b7d16c..538a960 100644 --- a/ts-src/bip32.ts +++ b/ts-src/bip32.ts @@ -35,6 +35,8 @@ export interface BIP32Interface { derivePath(path: string): BIP32Interface; sign(hash: Buffer, lowR?: boolean): Buffer; verify(hash: Buffer, signature: Buffer): boolean; + signSchnorr(hash: Buffer): Buffer; + verifySchnorr(hash: Buffer, signature: Buffer): boolean; } export interface BIP32API { @@ -63,12 +65,14 @@ export interface TinySecp256k1Interface { ): Uint8Array | null; privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; 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; + verifySchnorr(h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean; } export default function(ecc: TinySecp256k1Interface): BIP32API { @@ -340,9 +344,18 @@ export default function(ecc: TinySecp256k1Interface): BIP32API { } } + signSchnorr(hash: Buffer): Buffer { + if (!this.privateKey) throw new Error('Missing private key'); + return Buffer.from(ecc.signSchnorr(hash, this.privateKey)); + } + verify(hash: Buffer, signature: Buffer): boolean { return ecc.verify(hash, this.publicKey, signature); } + + verifySchnorr(hash: Buffer, signature: Buffer): boolean { + return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); + } } function fromBase58(inString: string, network?: Network): BIP32Interface { diff --git a/types/bip32.d.ts b/types/bip32.d.ts index 83fda9a..9a5950b 100644 --- a/types/bip32.d.ts +++ b/types/bip32.d.ts @@ -30,6 +30,8 @@ export interface BIP32Interface { derivePath(path: string): BIP32Interface; sign(hash: Buffer, lowR?: boolean): Buffer; verify(hash: Buffer, signature: Buffer): boolean; + signSchnorr(hash: Buffer): Buffer; + verifySchnorr(hash: Buffer, signature: Buffer): boolean; } export interface BIP32API { fromSeed(seed: Buffer, network?: Network): BIP32Interface; @@ -44,7 +46,9 @@ export interface TinySecp256k1Interface { pointAddScalar(p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null; privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; 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; + verifySchnorr(h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean; } export default function (ecc: TinySecp256k1Interface): BIP32API; export {}; From e9806c0ab3fc9d6858abca04568fd756d0b42c4d Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 11 Nov 2021 23:26:17 +0900 Subject: [PATCH 2/2] Simple sanity check on wrap --- src/bip32.js | 6 +++ src/testecc.js | 32 ++++++++++++ test/index.js | 15 +++++- ts-src/bip32.ts | 10 +++- ts-src/testecc.ts | 125 +++++++++++++++++++++++++++++++++++++++++++++ types/bip32.d.ts | 4 +- types/testecc.d.ts | 2 + 7 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 src/testecc.js create mode 100644 ts-src/testecc.ts create mode 100644 types/testecc.d.ts diff --git a/src/bip32.js b/src/bip32.js index deb6f09..71fe8b5 100644 --- a/src/bip32.js +++ b/src/bip32.js @@ -1,10 +1,12 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const crypto = require("./crypto"); +const testecc_1 = require("./testecc"); const bs58check = require('bs58check'); const typeforce = require('typeforce'); const wif = require('wif'); function default_1(ecc) { + testecc_1.testEcc(ecc); const UINT256_TYPE = typeforce.BufferN(32); const NETWORK_TYPE = typeforce.compile({ wif: typeforce.UInt8, @@ -212,12 +214,16 @@ function default_1(ecc) { signSchnorr(hash) { 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)); } verify(hash, signature) { return ecc.verify(hash, this.publicKey, signature); } verifySchnorr(hash, signature) { + if (!ecc.verifySchnorr) + throw new Error('verifySchnorr not supported by ecc library'); return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); } } diff --git a/src/testecc.js b/src/testecc.js new file mode 100644 index 0000000..e537398 --- /dev/null +++ b/src/testecc.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const h = (hex) => Buffer.from(hex, 'hex'); +function testEcc(ecc) { + assert(ecc.isPoint(h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))); + assert(!ecc.isPoint(h('030000000000000000000000000000000000000000000000000000000000000005'))); + assert(ecc.isPrivate(h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))); + // order - 1 + assert(ecc.isPrivate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))); + // 0 + assert(!ecc.isPrivate(h('0000000000000000000000000000000000000000000000000000000000000000'))); + // order + assert(!ecc.isPrivate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'))); + // order + 1 + assert(!ecc.isPrivate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142'))); + assert(Buffer.from(ecc.pointFromScalar(h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'))).equals(h('02b07ba9dca9523b7ef4bd97703d43d20399eb698e194704791a25ce77a400df99'))); + assert(Buffer.from(ecc.pointAddScalar(h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), h('0000000000000000000000000000000000000000000000000000000000000003'))).equals(h('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'))); + assert(Buffer.from(ecc.privateAdd(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), h('0000000000000000000000000000000000000000000000000000000000000002'))).equals(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))); + assert(Buffer.from(ecc.sign(h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))).equals(h('54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5'))); + assert(ecc.verify(h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), h('54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5'))); + if (ecc.signSchnorr) { + assert(Buffer.from(ecc.signSchnorr(h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), h('c90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9'), h('c87aa53824b4d7ae2eb035a2b5bbbccc080e76cdc6d1692c4b0b62d798e6d906'))).equals(h('5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7'))); + } + if (ecc.verifySchnorr) { + assert(ecc.verifySchnorr(h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), h('dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8'), h('5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7'))); + } +} +exports.testEcc = testEcc; +function assert(bool) { + if (!bool) + throw new Error('ecc library invalid'); +} diff --git a/test/index.js b/test/index.js index 2d44965..28414c0 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,11 @@ let BIP32Creator = require('..').default let tape = require('tape') let fixtures = require('./fixtures/index.json') -import('tiny-secp256k1').then(ecc => BIP32Creator(ecc)).then(BIP32 => { +let ecc +import('tiny-secp256k1').then(lib => { + ecc = lib + return BIP32Creator(lib) +}).then(BIP32 => { let LITECOIN = { wif: 0xb0, bip32: { @@ -95,6 +99,15 @@ validAll.forEach((ff) => { }) }) +tape('invalid ecc library throws', (t) => { + t.throws(() => { + BIP32Creator({ isPoint: () => false }) + }, /ecc library invalid/) + // Run with no schnorr and check it doesn't throw + BIP32Creator({ ...ecc, signSchnorr: null, verifySchnorr: null }) + t.end() +}) + tape('fromBase58 throws', (t) => { fixtures.invalid.fromBase58.forEach((f) => { t.throws(() => { diff --git a/ts-src/bip32.ts b/ts-src/bip32.ts index 538a960..1a53d20 100644 --- a/ts-src/bip32.ts +++ b/ts-src/bip32.ts @@ -1,4 +1,5 @@ import * as crypto from './crypto'; +import { testEcc } from './testecc'; const bs58check = require('bs58check'); const typeforce = require('typeforce'); const wif = require('wif'); @@ -65,17 +66,18 @@ export interface TinySecp256k1Interface { ): Uint8Array | null; privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; sign(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; - signSchnorr(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; - verifySchnorr(h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean; + verifySchnorr?(h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean; } export default function(ecc: TinySecp256k1Interface): BIP32API { + testEcc(ecc); const UINT256_TYPE = typeforce.BufferN(32); const NETWORK_TYPE = typeforce.compile({ wif: typeforce.UInt8, @@ -346,6 +348,8 @@ export default function(ecc: TinySecp256k1Interface): BIP32API { signSchnorr(hash: 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)); } @@ -354,6 +358,8 @@ export default function(ecc: TinySecp256k1Interface): BIP32API { } verifySchnorr(hash: Buffer, signature: Buffer): boolean { + if (!ecc.verifySchnorr) + throw new Error('verifySchnorr not supported by ecc library'); return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); } } diff --git a/ts-src/testecc.ts b/ts-src/testecc.ts new file mode 100644 index 0000000..2558e90 --- /dev/null +++ b/ts-src/testecc.ts @@ -0,0 +1,125 @@ +import { TinySecp256k1Interface } from './bip32'; + +const h = (hex: string): Buffer => Buffer.from(hex, 'hex'); + +export function testEcc(ecc: TinySecp256k1Interface): void { + assert( + ecc.isPoint( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + !ecc.isPoint( + h('030000000000000000000000000000000000000000000000000000000000000005'), + ), + ); + assert( + ecc.isPrivate( + h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + // order - 1 + assert( + ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ); + // 0 + assert( + !ecc.isPrivate( + h('0000000000000000000000000000000000000000000000000000000000000000'), + ), + ); + // order + assert( + !ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'), + ), + ); + // order + 1 + assert( + !ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142'), + ), + ); + assert( + Buffer.from( + ecc.pointFromScalar( + h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'), + )!, + ).equals( + h('02b07ba9dca9523b7ef4bd97703d43d20399eb698e194704791a25ce77a400df99'), + ), + ); + assert( + Buffer.from( + ecc.pointAddScalar( + h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h('0000000000000000000000000000000000000000000000000000000000000003'), + )!, + ).equals( + h('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'), + ), + ); + assert( + Buffer.from( + ecc.privateAdd( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), + h('0000000000000000000000000000000000000000000000000000000000000002'), + )!, + ).equals( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ); + assert( + Buffer.from( + ecc.sign( + h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + )!, + ).equals( + h( + '54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5', + ), + ), + ); + assert( + ecc.verify( + h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), + h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h( + '54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5', + ), + ), + ); + if (ecc.signSchnorr) { + assert( + Buffer.from( + ecc.signSchnorr( + h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), + h('c90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9'), + h('c87aa53824b4d7ae2eb035a2b5bbbccc080e76cdc6d1692c4b0b62d798e6d906'), + )!, + ).equals( + h( + '5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7', + ), + ), + ); + } + if (ecc.verifySchnorr) { + assert( + ecc.verifySchnorr( + h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), + h('dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8'), + h( + '5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7', + ), + ), + ); + } +} + +function assert(bool: boolean): void { + if (!bool) throw new Error('ecc library invalid'); +} diff --git a/types/bip32.d.ts b/types/bip32.d.ts index 9a5950b..00df325 100644 --- a/types/bip32.d.ts +++ b/types/bip32.d.ts @@ -46,9 +46,9 @@ export interface TinySecp256k1Interface { pointAddScalar(p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null; privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; sign(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; - signSchnorr(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; - verifySchnorr(h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean; + verifySchnorr?(h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean; } export default function (ecc: TinySecp256k1Interface): BIP32API; export {}; diff --git a/types/testecc.d.ts b/types/testecc.d.ts new file mode 100644 index 0000000..e3fd961 --- /dev/null +++ b/types/testecc.d.ts @@ -0,0 +1,2 @@ +import { TinySecp256k1Interface } from './bip32'; +export declare function testEcc(ecc: TinySecp256k1Interface): void;