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.
This commit is contained in:
Ryan X. Charles 2014-07-25 14:07:03 -07:00
parent 078d85ea19
commit f87da3b5ba
6 changed files with 309 additions and 15 deletions

View File

@ -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;

View File

@ -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');

View File

@ -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;

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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');