From f87da3b5baeb38c70086639c6b8f257bebbde4c2 Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Fri, 25 Jul 2014 14:07:03 -0700 Subject: [PATCH] add support for signing messages in compressed format ...this is the standard way to sign messages in bitcoin-qt. Note that the format of a compressed signature, for messages, is quite distinct from DER format, which is used in transactions. This commit also adds support for recovering the public key from a signature, which is necessary for this. The code for public key recover is taken from bitcoinjs-lib. --- lib/Key.js | 64 +++++++++++++++++++++++++++++++ lib/Message.js | 21 +++++++++++ lib/browser/Key.js | 59 +++++++++++++++++++++++++++++ lib/common/Key.js | 89 ++++++++++++++++++++++++++++++++++++-------- test/test.Key.js | 72 +++++++++++++++++++++++++++++++++++ test/test.Message.js | 19 ++++++++++ 6 files changed, 309 insertions(+), 15 deletions(-) diff --git a/lib/Key.js b/lib/Key.js index 6d69639..03bc8ec 100644 --- a/lib/Key.js +++ b/lib/Key.js @@ -1,9 +1,73 @@ var Key = require('bindings')('KeyModule').Key; var CommonKey = require('./common/Key'); +var bignum = require('bignum'); +var Point = require('./Point'); +var coinUtil = require('../util'); for (var i in CommonKey) { if (CommonKey.hasOwnProperty(i)) Key[i] = CommonKey[i]; } +Key.sign = function(hash, priv, k) { + if (k) + throw new Error('Deterministic k not supported in node'); + + var key = new Key(); + key.private = priv.toBuffer({size: 32}); + var sig = key.signSync(hash); + + var parsed = Key.parseDERsig(sig); + + return {r: parsed.r, s: parsed.s}; +}; + +Key.signCompressed = function(hash, priv, k) { + var sig = Key.sign(hash, priv, k); + var r = sig.r; + var s = sig.s; + var e = bignum.fromBuffer(hash); + + var G = Point.getG(); + var Q = Point.multiply(G, priv.toBuffer({size: 32})); + + var i = Key.calcPubKeyRecoveryParam(e, r, s, Q); + + var rbuf = r.toBuffer({size: 32}); + var sbuf = s.toBuffer({size: 32}); + var ibuf = new Buffer([i]); + var buf = Buffer.concat([ibuf, rbuf, sbuf]); + return buf; +}; + +Key.verifyCompressed = function(hash, sigbuf, pubkeyhash) { + if (sigbuf.length !== 1 + 32 + 32) + throw new Error("Invalid length for sigbuf"); + + var i = sigbuf[0]; + if (i < 0 || i > 3) + throw new Error("Invalid value for i"); + + var rbuf = sigbuf.slice(1, 1 + 32); + var sbuf = sigbuf.slice(1 + 32, 1 + 32 + 32); + var r = bignum.fromBuffer(rbuf); + var s = bignum.fromBuffer(sbuf); + + var sigDER = Key.rs2DER(r, s); + + var e = bignum.fromBuffer(hash); + + var key = new Key(); + var pub = Key.recoverPubKey(e, r, s, i); + var pubbuf = pub.toCompressedPubKey(); + key.public = pubbuf; + + var pubkeyhash2 = coinUtil.sha256ripe160(pubbuf); + if (pubkeyhash2.toString('hex') !== pubkeyhash.toString('hex')) { + return false; + } + + return key.verifySignatureSync(hash, sigDER); +}; + module.exports = Key; diff --git a/lib/Message.js b/lib/Message.js index ac0cb7f..333ebcb 100644 --- a/lib/Message.js +++ b/lib/Message.js @@ -1,15 +1,20 @@ 'use strict'; var coinUtil = require('../util'); var Key = require('./Key'); +var bignum = require('bignum'); +var coinUtil = require('../util'); var Message = function() {}; +//creates DER format signatures. +//probably not what you want. Message.sign = function(str, key) { var hash = Message.magicHash(str); var sig = key.signSync(hash); return sig; }; +//verifies compressed signatures Message.verifyWithPubKey = function(pubkey, message, sig) { var hash = Message.magicHash(message); var key = new Key(); @@ -20,6 +25,22 @@ Message.verifyWithPubKey = function(pubkey, message, sig) { return key.verifySignatureSync(hash, sig); }; +//creates compressed format signatures. +//you probably want this, not .sign +Message.signMessage = function(str, key) { + var hash = Message.magicHash(str); + var privnum = bignum.fromBuffer(key.private); + var sig = Key.signCompressed(hash, privnum); + return sig; +}; + +//verifies compressed signatures +Message.verifyMessage = function(pubkeyhash, message, sig) { + var hash = Message.magicHash(message); + + return Key.verifyCompressed(hash, sig, pubkeyhash); +}; + //TODO: Message.verify ... with address, not pubkey Message.magicBytes = new Buffer('Bitcoin Signed Message:\n'); diff --git a/lib/browser/Key.js b/lib/browser/Key.js index 6de6edc..088c64b 100644 --- a/lib/browser/Key.js +++ b/lib/browser/Key.js @@ -149,4 +149,63 @@ Key.prototype.verifySignatureSync = function(hash, sig) { return v; }; +Key.sign = function(hash, priv, k) { + var d = priv; + var n = Point.getN(); + var e = new bignum(hash); + + do { + var k = k || Key.genk(); + var G = Point.getG(); + var Q = Point.multiply(G, k); + var r = Q.x.mod(n); + var s = k.invm(n).mul(e.add(d.mul(r))).mod(n); + } while (r.cmp(new bignum(0)) <= 0 || s.cmp(new bignum(0)) <= 0); + + return {r: r, s: s}; +}; + +Key.signCompressed = function(hash, priv, k) { + var sig = Key.sign(hash, priv, k); + var r = sig.r; + var s = sig.s; + var e = bignum.fromBuffer(hash); + + var G = Point.getG(); + var Q = Point.multiply(G, priv); + + var i = Key.calcPubKeyRecoveryParam(e, r, s, Q); + + var rbuf = r.toBuffer({size: 32}); + var sbuf = s.toBuffer({size: 32}); + var ibuf = new Buffer([i]); + var buf = Buffer.concat([ibuf, rbuf, sbuf]); + return buf; +}; + +Key.verifyCompressed = function(hash, sigbuf) { + if (sigbuf.length !== 1 + 32 + 32) + throw new Error("Invalid length for sigbuf"); + + var i = sigbuf[0]; + if (i < 0 || i > 3) + throw new Error("Invalid value for i"); + + var rbuf = sigbuf.slice(1, 1 + 32); + var sbuf = sigbuf.slice(1 + 32, 1 + 32 + 32); + var r = bignum.fromBuffer(rbuf); + var s = bignum.fromBuffer(sbuf); + + var sigDER = Key.rs2DER(r, s); + + var e = bignum.fromBuffer(hash); + + var key = new Key(); + var pub = Key.recoverPubKey(e, r, s, i); + var pubbuf = pub.toCompressedPubKey(); + key.public = pubbuf; + + return key.verifySignatureSync(hash, sigDER); +}; + module.exports = Key; diff --git a/lib/common/Key.js b/lib/common/Key.js index 0665c49..160d030 100644 --- a/lib/common/Key.js +++ b/lib/common/Key.js @@ -1,6 +1,8 @@ var bignum = require('bignum'); -var Point = require('./Point'); -var SecureRandom = require('./SecureRandom'); +var Point = require('../Point'); +var SecureRandom = require('../SecureRandom'); +var bignum = require('bignum'); +var elliptic = require('elliptic'); var Key = function() {} Key.parseDERsig = function(sig) { @@ -82,21 +84,78 @@ Key.rs2DER = function(r, s) { return der; }; -Key.sign = function(hash, priv, k) { - var d = priv; - var n = Point.getN(); - var e = new bignum(hash); +Key.recoverPubKey = function(e, r, s, i) { + var bnjs = require('bn.js'); - do { - var k = k || Key.genk(); - var G = Point.getG(); - var Q = Point.multiply(G, k); - var r = Q.x.mod(n); - var s = k.invm(n).mul(e.add(d.mul(r))).mod(n); - } while (r.cmp(new bignum(0)) <= 0 || s.cmp(new bignum(0)) <= 0); + if (i>3 || i<0) + throw new Error('Recovery param is more than two bits'); - return {r: r, s: s}; -}; + e = new bnjs(e.toBuffer({size: 32})); + r = new bnjs(r.toBuffer({size: 32})); + s = new bnjs(s.toBuffer({size: 32})); + + var ec = elliptic.curves.secp256k1; + + // A set LSB signifies that the y-coordinate is odd + var isYOdd = i & 1; + + // The more significant bit specifies whether we should use the + // first or second candidate key. + var isSecondKey = i >> 1; + + var n = ec.curve.n; + var G = ec.curve.g; + + // 1.1 Let x = r + jn + var x = isSecondKey ? r.add(n) : r; + var R = ec.curve.pointFromX(isYOdd, x.toArray()); + + // 1.4 Check that nR is at infinity + var nR = R.mul(n); + + //TODO: check that nR is not infinity + //assert(curve.isInfinity(nR), 'nR is not a valid curve point'); + + // Compute -e from e + var eNeg = e.neg().mod(n); + + // 1.6.1 Compute Q = r^-1 (sR - eG) + // Q = r^-1 (sR + -eG) + var rInv = r.invm(n); + + //var Q = R.multiplyTwo(s, G, eNeg).mul(rInv); + var Q = R.mul(s).add(G.mul(eNeg)).mul(rInv); + ec.curve.validate(Q); + var pubkey = new Point(); + pubkey.x = bignum(Q.x.toString()); + pubkey.y = bignum(Q.y.toString()); + + return pubkey; +} + +/** +* Calculate pubkey extraction parameter. +* +* When extracting a pubkey from a signature, we have to +* distinguish four different cases. Rather than putting this +* burden on the verifier, Bitcoin includes a 2-bit value with the +* signature. +* +* This function simply tries all four cases and returns the value +* that resulted in a successful pubkey recovery. +*/ +Key.calcPubKeyRecoveryParam = function(e, r, s, Q) { + for (var i = 0; i < 4; i++) { + var Qprime = Key.recoverPubKey(e, r, s, i); + + // 1.6.2 Verify Q + if (Qprime.x.toString() == Q.x.toString() && Qprime.y.toString() == Q.y.toString()) { + return i; + } + } + + throw new Error('Unable to find valid recovery factor'); +} Key.genk = function() { //TODO: account for when >= n diff --git a/test/test.Key.js b/test/test.Key.js index 244035c..b97fbd4 100644 --- a/test/test.Key.js +++ b/test/test.Key.js @@ -278,4 +278,76 @@ describe('Key (ECKey)', function() { }); }); + describe('#recoverPubKey', function() { + var key = new bitcore.Key(); + key.private = bitcore.util.sha256('test'); + key.regenerateSync(); + var data = bitcore.util.sha256('some data'); + var privnum = bignum.fromBuffer(key.private); + var sig = Key.sign(data, privnum); + var r = sig.r; + var s = sig.s; + var e = bignum.fromBuffer(data); + + it('should return a point', function() { + var Q = Key.recoverPubKey(e, r, s, 0); + should.exist(Q.x); + should.exist(Q.y); + }); + }); + + describe('calcPubKeyRecoveryParam', function() { + var key = new bitcore.Key(); + key.private = bitcore.util.sha256('test'); + key.regenerateSync(); + key.compressed = false; + var pubnum = Point.fromUncompressedPubKey(key.public); + var data = bitcore.util.sha256('some data'); + var privnum = bignum.fromBuffer(key.private); + var sig = Key.sign(data, privnum); + var r = sig.r; + var s = sig.s; + var knownr = bignum('71706645040721865894779025947914615666559616020894583599959600180037551395766'); + var knowns = bignum('109412465507152403114191008482955798903072313614214706891149785278625167723646'); + var e = bignum.fromBuffer(data); + + it('should return a number', function() { + var i = Key.calcPubKeyRecoveryParam(e, r, s, pubnum); + (i >= 0 || i <= 3).should.equal(true); + }); + + it('should return x for these known values', function() { + var i = Key.calcPubKeyRecoveryParam(e, knownr, knowns, pubnum); + i.should.equal(1); + }); + }); + + describe('#signCompressed', function() { + var key = new bitcore.Key(); + key.private = bitcore.util.sha256('test'); + key.regenerateSync(); + var data = bitcore.util.sha256('some data'); + var privnum = bignum.fromBuffer(key.private); + + it('should return a 65 byte buffer', function() { + var sig = Key.signCompressed(data, privnum); + Buffer.isBuffer(sig).should.equal(true); + sig.length.should.equal(65); + }); + }); + + describe('#verifyCompressed', function() { + var key = new bitcore.Key(); + key.private = bitcore.util.sha256('test'); + key.regenerateSync(); + var pubkeyhash = bitcore.util.sha256ripe160(key.public); + var data = bitcore.util.sha256('some data'); + var privnum = bignum.fromBuffer(key.private); + var sig = Key.signCompressed(data, privnum); + + it('should verify that which was signed compressed', function() { + Key.verifyCompressed(data, sig, pubkeyhash).should.equal(true); + }); + }); + }); diff --git a/test/test.Message.js b/test/test.Message.js index c6b54b9..1a15a27 100644 --- a/test/test.Message.js +++ b/test/test.Message.js @@ -46,6 +46,25 @@ describe('Message', function() { }); }); + describe('#signMessage', function() { + it('should return a 65 byte buffer', function() { + var message = 'my message'; + var key = bitcore.Key.generateSync(); + var sig = Message.signMessage(message, key); + sig.length.should.equal(65); + }); + }); + + describe('#verifyMessage', function() { + it('should return a 65 byte buffer', function() { + var message = 'my message'; + var key = bitcore.Key.generateSync(); + var sig = Message.signMessage(message, key); + var pubkeyhash = bitcore.util.sha256ripe160(key.public); + Message.verifyMessage(pubkeyhash, message, sig).should.equal(true); + }); + }); + describe('magicBytes', function() { it('should be "Bitcoin Signed Message:\\n"', function() { Message.magicBytes.toString().should.equal('Bitcoin Signed Message:\n');