From a0726efe750c5d3fdc58d2a77ccd53a2515682e0 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 1 Sep 2016 02:06:08 -0700 Subject: [PATCH] crypto: preliminary support for schnorr signatures. --- lib/crypto/schnorr.js | 290 ++++++++++++++++++++++++++++++++++++++++++ test/utils-test.js | 10 ++ 2 files changed, 300 insertions(+) create mode 100644 lib/crypto/schnorr.js diff --git a/lib/crypto/schnorr.js b/lib/crypto/schnorr.js new file mode 100644 index 00000000..abfd26f6 --- /dev/null +++ b/lib/crypto/schnorr.js @@ -0,0 +1,290 @@ +/*! + * schnorr.js - schnorr signatures for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bn = require('bn.js'); +var elliptic = require('elliptic'); +var Signature = require('elliptic/lib/elliptic/ec/signature'); +var ec = require('./ec'); +var curve = elliptic.ec('secp256k1').curve; +var sha256 = require('./crypto').sha256; + +/** + * @exports schnorr + */ + +var schnorr = exports; + +/** + * Hash (r | M). + * @param {Buffer} msg + * @param {BN} r + * @param {Function?} hash + * @returns {Buffer} + */ + +schnorr.hash = function _hash(msg, r, hash) { + var R = r.toArrayLike(Buffer, 'be', 32); + var B = new Buffer(64); + var H; + + if (!hash) + hash = sha256; + + R.copy(B, 0); + msg.copy(B, 32); + H = hash(B); + + return new bn(H); +}; + +/** + * Sign message. + * @param {Buffer} msg + * @param {BN} priv + * @param {BN} k + * @param {Function|null} hash + * @param {Buffer} pubnonce + * @returns {Signature|null} + */ + +schnorr._sign = function _sign(msg, prv, k, hash, pubnonce) { + var r, pn, h, s; + + if (k.cmpn(0) === 0) + return; + + if (k.cmp(curve.n) >= 0) + return; + + r = curve.g.mul(k); + + if (pubnonce) { + pn = curve.decodePoint(pubnonce); + r = r.add(pn); + } + + if (r.y.isOdd()) { + k = k.umod(curve.n); + k = curve.n.sub(k); + } + + h = schnorr.hash(msg, r.getX(), hash); + + if (h.cmp(curve.n) >= 0) + return; + + s = h.imul(prv); + s = k.isub(s); + s = s.umod(curve.n); + + if (s.cmpn(0) === 0) + return; + + return new Signature({ r: r.getX(), s: s }); +}; + +/** + * Sign message. + * @param {Buffer} msg + * @param {Buffer} key + * @param {Function?} hash + * @param {Buffer} pubnonce + * @returns {Signature} + */ + +schnorr.sign = function sign(msg, key, hash, pubnonce) { + var prv = new bn(key); + var k, sig; + + if (prv.cmpn(0) === 0) + throw new Error('Bad private key.'); + + if (prv.cmp(curve.n) >= 0) + throw new Error('Bad private key.'); + + while (!sig) { + k = new bn(ec.random(32)); + sig = schnorr._sign(msg, prv, k, hash, pubnonce); + } + + return sig; +}; + +/** + * Verify signature. + * @param {Buffer} msg + * @param {Buffer} signature + * @param {Buffer} key + * @param {Function?} hash + * @returns {Buffer} + */ + +schnorr.verify = function verify(msg, signature, key, hash) { + var sig = new Signature(signature); + var h = schnorr.hash(msg, sig.r, hash); + var k, l, r, rl; + + if (h.cmp(curve.n) >= 0) + throw new Error('Invalid hash.'); + + if (h.cmpn(0) === 0) + throw new Error('Invalid hash.'); + + if (sig.s.cmp(curve.n) >= 0) + throw new Error('Invalid S value.'); + + if (sig.r.cmp(curve.p) > 0) + throw new Error('Invalid R value.'); + + k = curve.decodePoint(key); + l = k.mul(h); + r = curve.g.mul(sig.s); + rl = l.add(r); + + if (rl.y.isOdd()) + throw new Error('Odd R value.'); + + return rl.getX().cmp(sig.r) === 0; +}; + +/** + * Recover public key. + * @param {Buffer} msg + * @param {Buffer} signature + * @param {Function?} hash + * @returns {Buffer} + */ + +schnorr.recover = function recover(signature, msg, hash) { + var sig = new Signature(signature); + var h = schnorr.hash(msg, sig.r, hash); + var hinv, s, R, l, r, k, rl; + + if (h.cmp(curve.n) >= 0) + throw new Error('Invalid hash.'); + + if (h.cmpn(0) === 0) + throw new Error('Invalid hash.'); + + if (sig.s.cmp(curve.n) >= 0) + throw new Error('Invalid S value.'); + + if (sig.r.cmp(curve.p) > 0) + throw new Error('Invalid R value.'); + + hinv = h.invm(curve.n); + hinv = hinv.umod(curve.n); + + s = sig.s; + s = curve.n.sub(s); + s = s.umod(curve.n); + + s = s.imul(hinv); + s = s.umod(curve.n); + + R = curve.pointFromX(sig.r, false); + l = R.mul(hinv); + r = curve.g.mul(s); + k = l.add(r); + + l = k.mul(h); + r = curve.g.mul(sig.s); + rl = l.add(r); + + if (rl.y.isOdd()) + throw new Error('Odd R value.'); + + if (rl.getX().cmp(sig.r) !== 0) + throw new Error('Could not recover pubkey.'); + + return new Buffer(k.encode('array', true)); +}; + +/** + * Combine signatures. + * @param {Buffer[]} sigs + * @returns {Signature} + */ + +schnorr.combineSigs = function combineSigs(sigs) { + var s = new bn(0); + var i, r, sig, last; + + for (i = 0; i < sigs.length; i++) { + sig = new Signature(sigs[i]); + + if (sig.s.cmpn(0) === 0) + throw new Error('Bad S value.'); + + if (sig.s.cmp(curve.n) >= 0) + throw new Error('Bad S value.'); + + if (!r) + r = sig.r; + + if (last && last.r.cmp(sig.r) !== 0) + throw new Error('Bad signature combination.'); + + s.iadd(sig.s); + s = s.umod(curve.n); + + last = sig; + } + + if (s.cmpn(0) === 0) + throw new Error('Bad combined signature.'); + + return new Signature({ r: r, s: s }); +}; + +/** + * Combine public keys. + * @param {Buffer[]} keys + * @returns {Buffer} + */ + +schnorr.combineKeys = function combineKeys(keys) { + var i, key, point; + + if (keys.length === 0) + throw new Error(); + + if (keys.length === 1) + return keys[0]; + + point = curve.decodePoint(keys[0]); + + for (i = 1; i < keys.length; i++) { + key = curve.decodePoint(keys[i]); + point = point.add(key); + } + + return new Buffer(point.encode('array', true)); +}; + +/** + * Partially sign. + * @param {Buffer} msg + * @param {Buffer} priv + * @param {Buffer} privnonce + * @param {Buffer} pubs + * @param {Function?} hash + * @returns {Buffer} + */ + +schnorr.partialSign = function partialSign(msg, priv, privnonce, pubs, hash) { + var prv = new bn(priv); + + if (prv.cmpn(0) === 0) + throw new Error('Bad private key.'); + + if (prv.cmp(curve.n) >= 0) + throw new Error('Bad private key.'); + + return schnorr._sign(msg, prv, new bn(privnonce), hash, pubs); +}; diff --git a/test/utils-test.js b/test/utils-test.js index 72caacd3..e1ca035e 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -4,6 +4,7 @@ var bn = require('bn.js'); var bcoin = require('../').set('main'); var assert = require('assert'); var utils = bcoin.utils; +var schnorr = require('../lib/crypto/schnorr'); describe('Utils', function() { var vectors = [ @@ -290,4 +291,13 @@ describe('Utils', function() { assert.equal(prk.toString('hex'), prkE); assert.equal(okm.toString('hex'), okmE); }); + + it('should do proper schnorr', function() { + var key = bcoin.ec.generatePrivateKey(); + var pub = bcoin.ec.publicKeyCreate(key, true); + var msg = utils.hash256(new Buffer('foo', 'ascii')); + var sig = schnorr.sign(msg, key); + assert(schnorr.verify(msg, sig, pub)); + assert.deepEqual(schnorr.recover(sig, msg), pub); + }); });