Merge pull request #459 from ryanxcharles/feature/message-signing
add support for signing messages in compressed format
This commit is contained in:
commit
7d03056e09
64
lib/Key.js
64
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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue