From a66773b5f58d80917ef10e4675df4b471b4b9d5f Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 5 Dec 2014 15:07:30 +1100 Subject: [PATCH 1/5] tests/integration: add HDNode private key recovery example --- src/hdnode.js | 4 +-- test/integration/crypto.js | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 test/integration/crypto.js diff --git a/src/hdnode.js b/src/hdnode.js index a3c9f4b..03e5bb1 100644 --- a/src/hdnode.js +++ b/src/hdnode.js @@ -37,6 +37,7 @@ function HDNode(K, chainCode, network) { this.chainCode = chainCode this.depth = 0 this.index = 0 + this.parentFingerprint = 0x00000000 this.network = network if (K instanceof BigInteger) { @@ -196,8 +197,7 @@ HDNode.prototype.toBuffer = function(isPrivate, __ignoreDeprecation) { buffer.writeUInt8(this.depth, 4) // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) - var fingerprint = (this.depth === 0) ? 0x00000000 : this.parentFingerprint - buffer.writeUInt32BE(fingerprint, 5) + buffer.writeUInt32BE(this.parentFingerprint, 5) // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. // This is encoded in Big endian. (0x00000000 if master key) diff --git a/test/integration/crypto.js b/test/integration/crypto.js new file mode 100644 index 0000000..38389b7 --- /dev/null +++ b/test/integration/crypto.js @@ -0,0 +1,52 @@ +var assert = require('assert') +var bigi = require('bigi') +var bitcoin = require('../../') +var crypto = require('crypto') + +describe('bitcoinjs-lib (crypto)', function() { + it('can recover a parent private key from the parent\'s public key and a derived non-hardened child private key', function() { + function recoverParent(master, child) { + assert(!master.privKey, 'You already have the parent private key') + assert(child.privKey, 'Missing child private key') + + var curve = bitcoin.ECKey.curve + var QP = master.pubKey.toBuffer() + var QP64 = QP.toString('base64') + var d1 = child.privKey.d + var d2 + var indexBuffer = new Buffer(4) + + // search index space until we find it + for (var i = 0; i < bitcoin.HDNode.HIGHEST_BIT; ++i) { + indexBuffer.writeUInt32BE(i, 0) + + // calculate I + var data = Buffer.concat([QP, indexBuffer]) + var I = crypto.createHmac('sha512', master.chainCode).update(data).digest() + var IL = I.slice(0, 32) + var pIL = bigi.fromBuffer(IL) + + // See hdnode.js:273 to understand + d2 = d1.subtract(pIL).mod(curve.n) + + var Qp = new bitcoin.ECKey(d2, true).pub.toBuffer() + if (Qp.toString('base64') === QP64) break + } + + var node = new bitcoin.HDNode(d2, master.chainCode, master.network) + node.depth = master.depth + node.index = master.index + node.masterFingerprint = master.masterFingerprint + return node + } + + var seed = crypto.randomBytes(32) + var master = bitcoin.HDNode.fromSeedBuffer(seed) + var child = master.derive(6) // m/6 + + // now for the recovery + var neuteredMaster = master.neutered() + var recovered = recoverParent(neuteredMaster, child) + assert.equal(recovered.toBase58(), master.toBase58()) + }) +}) From de914ff8faeee2ac1ba0e8d46492a86bc47e3805 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 5 Dec 2014 15:09:24 +1100 Subject: [PATCH 2/5] tests/integration: move crypto-like tests to test/integration/crypto.js --- test/integration/advanced.js | 36 ------------------------------------ test/integration/crypto.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/test/integration/advanced.js b/test/integration/advanced.js index be6af1f..a6479f3 100644 --- a/test/integration/advanced.js +++ b/test/integration/advanced.js @@ -1,5 +1,4 @@ var assert = require('assert') -var bigi = require('bigi') var bitcoin = require('../../') var helloblock = require('helloblock-js')({ network: 'testnet' @@ -22,41 +21,6 @@ describe('bitcoinjs-lib (advanced)', function() { assert(bitcoin.Message.verify(address, signature, message)) }) - it('can generate a single-key stealth address', function() { - var receiver = bitcoin.ECKey.fromWIF('5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss') - - // XXX: ephemeral, must be random (and secret to sender) to preserve privacy - var sender = bitcoin.ECKey.fromWIF('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct') - - var G = bitcoin.ECKey.curve.G - var d = receiver.d // secret (receiver only) - var Q = receiver.pub.Q // shared - - var e = sender.d // secret (sender only) - var P = sender.pub.Q // shared - - // derived shared secret - var eQ = Q.multiply(e) // sender - var dP = P.multiply(d) // receiver - assert.deepEqual(eQ.getEncoded(), dP.getEncoded()) - - var c = bigi.fromBuffer(bitcoin.crypto.sha256(eQ.getEncoded())) - var cG = G.multiply(c) - - // derived public key - var QprimeS = Q.add(cG) - var QprimeR = G.multiply(d.add(c)) - assert.deepEqual(QprimeR.getEncoded(), QprimeS.getEncoded()) - - // derived shared-secret address - var address = new bitcoin.ECPubKey(QprimeS).getAddress().toString() - - assert.equal(address, '1EwCNJNZM5q58YPPTnjR1H5BvYRNeyZi47') - }) - - // TODO - it.skip('can generate a dual-key stealth address', function() {}) - it('can create an OP_RETURN transaction', function(done) { this.timeout(20000) diff --git a/test/integration/crypto.js b/test/integration/crypto.js index 38389b7..7d53602 100644 --- a/test/integration/crypto.js +++ b/test/integration/crypto.js @@ -4,6 +4,41 @@ var bitcoin = require('../../') var crypto = require('crypto') describe('bitcoinjs-lib (crypto)', function() { + it('can generate a single-key stealth address', function() { + var receiver = bitcoin.ECKey.fromWIF('5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss') + + // XXX: ephemeral, must be random (and secret to sender) to preserve privacy + var sender = bitcoin.ECKey.fromWIF('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct') + + var G = bitcoin.ECKey.curve.G + var d = receiver.d // secret (receiver only) + var Q = receiver.pub.Q // shared + + var e = sender.d // secret (sender only) + var P = sender.pub.Q // shared + + // derived shared secret + var eQ = Q.multiply(e) // sender + var dP = P.multiply(d) // receiver + assert.deepEqual(eQ.getEncoded(), dP.getEncoded()) + + var c = bigi.fromBuffer(bitcoin.crypto.sha256(eQ.getEncoded())) + var cG = G.multiply(c) + + // derived public key + var QprimeS = Q.add(cG) + var QprimeR = G.multiply(d.add(c)) + assert.deepEqual(QprimeR.getEncoded(), QprimeS.getEncoded()) + + // derived shared-secret address + var address = new bitcoin.ECPubKey(QprimeS).getAddress().toString() + + assert.equal(address, '1EwCNJNZM5q58YPPTnjR1H5BvYRNeyZi47') + }) + + // TODO + it.skip('can generate a dual-key stealth address', function() {}) + it('can recover a parent private key from the parent\'s public key and a derived non-hardened child private key', function() { function recoverParent(master, child) { assert(!master.privKey, 'You already have the parent private key') From e6b7f51055e35ff93c4616dd4811709a3ed2b424 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 8 Dec 2014 11:58:46 +1100 Subject: [PATCH 3/5] README: update README links --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cf67965..329e7c7 100644 --- a/README.md +++ b/README.md @@ -73,11 +73,12 @@ The below examples are implemented as integration tests, they should be very eas - [Create a Transaction](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/basic.js#L36) - [Sign a Bitcoin message](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/advanced.js#L9) - [Verify a Bitcoin message](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/advanced.js#L17) -- [Generate a single-key stealth address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/advanced.js#L25) -- [Generate a dual-key stealth address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/advanced.js#L58) -- [Create an OP RETURN transaction](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/advanced.js#L60) +- [Create an OP RETURN transaction](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/advanced.js#L24) - [Create a 2-of-3 multisig P2SH address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/multisig.js#L8) - [Spend from a 2-of-2 multisig P2SH address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/multisig.js#L22) +- [Generate a single-key stealth address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/crypto.js#L7) +- [Generate a dual-key stealth address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/crypto.js#L40) +- [Recover a BIP32 parent private key from the parent public key and a derived non-hardened child private key](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/crypto.js#L42) ## Projects utilizing BitcoinJS From 837e0a3564716b0147e00e93a2128d4b353fbe49 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 9 Dec 2014 11:54:36 +1100 Subject: [PATCH 4/5] package: use cb-helloblock for integration tests --- package.json | 3 ++- test/integration/advanced.js | 19 ++++++++----------- test/integration/multisig.js | 16 +++++++--------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 6b11f97..9880116 100644 --- a/package.json +++ b/package.json @@ -54,10 +54,11 @@ "ecurve": "1.0.0" }, "devDependencies": { + "async": "^0.9.0", "browserify": "^5.12.0", "bs58": "^2.0.0", + "cb-helloblock": "^0.4.7", "coveralls": "^2.11.2", - "helloblock-js": "^0.2.5", "istanbul": "^0.3.2", "jshint": "^2.5.6", "mocha": "^1.21.4", diff --git a/test/integration/advanced.js b/test/integration/advanced.js index a6479f3..b4effbb 100644 --- a/test/integration/advanced.js +++ b/test/integration/advanced.js @@ -1,8 +1,6 @@ var assert = require('assert') var bitcoin = require('../../') -var helloblock = require('helloblock-js')({ - network: 'testnet' -}) +var blockchain = new (require('cb-helloblock'))('testnet') describe('bitcoinjs-lib (advanced)', function() { it('can sign a Bitcoin message', function() { @@ -27,10 +25,10 @@ describe('bitcoinjs-lib (advanced)', function() { var key = bitcoin.ECKey.fromWIF("L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy") var address = key.pub.getAddress(bitcoin.networks.testnet).toString() - helloblock.faucet.withdraw(address, 2e4, function(err) { + blockchain.addresses.__faucetWithdraw(address, 2e4, function(err) { if (err) return done(err) - helloblock.addresses.getUnspents(address, function(err, _, unspents) { + blockchain.addresses.unspents(address, function(err, unspents) { if (err) return done(err) // filter small unspents @@ -44,20 +42,19 @@ describe('bitcoinjs-lib (advanced)', function() { var data = new Buffer('cafedeadbeef', 'hex') var dataScript = bitcoin.scripts.nullDataOutput(data) - tx.addInput(unspent.txHash, unspent.index) + tx.addInput(unspent.txId, unspent.vout) tx.addOutput(dataScript, 1000) tx.sign(0, key) - helloblock.transactions.propagate(tx.build().toHex(), function(err) { + blockchain.transactions.propagate(tx.build().toHex(), function(err) { if (err) return done(err) // check that the message was propagated - helloblock.addresses.getTransactions(address, function(err, res, transactions) { + blockchain.addresses.transactions(address, function(err, transactions) { if (err) return done(err) - var transaction = transactions[0] - var output = transaction.outputs[0] - var dataScript2 = bitcoin.Script.fromHex(output.scriptPubKey) + var transaction = bitcoin.Transaction.fromHex(transactions[0].txHex) + var dataScript2 = transaction.outs[0].script var data2 = dataScript2.chunks[1] assert.deepEqual(dataScript, dataScript2) diff --git a/test/integration/multisig.js b/test/integration/multisig.js index 60f46c9..911affd 100644 --- a/test/integration/multisig.js +++ b/test/integration/multisig.js @@ -1,8 +1,6 @@ var assert = require('assert') var bitcoin = require('../../') -var helloblock = require('helloblock-js')({ - network: 'testnet' -}) +var blockchain = new (require('cb-helloblock'))('testnet') describe('bitcoinjs-lib (multisig)', function() { it('can create a 2-of-3 multisig P2SH address', function() { @@ -33,11 +31,11 @@ describe('bitcoinjs-lib (multisig)', function() { var address = bitcoin.Address.fromOutputScript(scriptPubKey, bitcoin.networks.testnet).toString() // Attempt to send funds to the source address - helloblock.faucet.withdraw(address, 2e4, function(err) { + blockchain.addresses.__faucetWithdraw(address, 2e4, function(err) { if (err) return done(err) // get latest unspents from the address - helloblock.addresses.getUnspents(address, function(err, _, unspents) { + blockchain.addresses.unspents(address, function(err, unspents) { if (err) return done(err) // filter small unspents @@ -50,7 +48,7 @@ describe('bitcoinjs-lib (multisig)', function() { var targetAddress = bitcoin.ECKey.makeRandom().pub.getAddress(bitcoin.networks.testnet).toString() var txb = new bitcoin.TransactionBuilder() - txb.addInput(unspent.txHash, unspent.index) + txb.addInput(unspent.txId, unspent.vout) txb.addOutput(targetAddress, 1e4) // sign w/ each private key @@ -59,14 +57,14 @@ describe('bitcoinjs-lib (multisig)', function() { }) // broadcast our transaction - helloblock.transactions.propagate(txb.build().toHex(), function(err) { + blockchain.transactions.propagate(txb.build().toHex(), function(err) { if (err) return done(err) // check that the funds (1e4 Satoshis) indeed arrived at the intended address - helloblock.addresses.get(targetAddress, function(err, res, addrInfo) { + blockchain.addresses.summary(targetAddress, function(err, result) { if (err) return done(err) - assert.equal(addrInfo.balance, 1e4) + assert.equal(result.balance, 1e4) done() }) }) From 3710105eefee476a0a904967995cb0a92bde5530 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 9 Dec 2014 11:52:01 +1100 Subject: [PATCH 5/5] tests/integration: add k-value derivation and private key recovery example --- README.md | 1 + test/integration/crypto.js | 96 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/README.md b/README.md index 329e7c7..e941e4f 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ The below examples are implemented as integration tests, they should be very eas - [Generate a single-key stealth address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/crypto.js#L7) - [Generate a dual-key stealth address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/crypto.js#L40) - [Recover a BIP32 parent private key from the parent public key and a derived non-hardened child private key](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/crypto.js#L42) +- [Recover a Private key from duplicate R values in a signature](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/crypto.js#L90) ## Projects utilizing BitcoinJS diff --git a/test/integration/crypto.js b/test/integration/crypto.js index 7d53602..06e3cf1 100644 --- a/test/integration/crypto.js +++ b/test/integration/crypto.js @@ -1,6 +1,8 @@ var assert = require('assert') +var async = require('async') var bigi = require('bigi') var bitcoin = require('../../') +var blockchain = new (require('cb-helloblock'))('bitcoin') var crypto = require('crypto') describe('bitcoinjs-lib (crypto)', function() { @@ -84,4 +86,98 @@ describe('bitcoinjs-lib (crypto)', function() { var recovered = recoverParent(neuteredMaster, child) assert.equal(recovered.toBase58(), master.toBase58()) }) + + it('can recover a private key from duplicate R values', function() { + var inputs = [ + { + txId: "f4c16475f2a6e9c602e4a287f9db3040e319eb9ece74761a4b84bc820fbeef50", + vout: 0 + }, + { + txId: "f4c16475f2a6e9c602e4a287f9db3040e319eb9ece74761a4b84bc820fbeef50", + vout: 1 + } + ] + + var txIds = inputs.map(function(x) { return x.txId }) + + // first retrieve the relevant transactions + blockchain.transactions.get(txIds, function(err, results) { + assert.ifError(err) + + var transactions = {} + results.forEach(function(tx) { + transactions[tx.txId] = bitcoin.Transaction.fromHex(tx.txHex) + }) + + var tasks = [] + + // now we need to collect/transform a bit of data from the selected inputs + inputs.forEach(function(input) { + var transaction = transactions[input.txId] + var script = transaction.ins[input.vout].script + assert(bitcoin.scripts.isPubKeyHashInput(script), 'Expected pubKeyHash script') + + var prevOutTxId = bitcoin.bufferutils.reverse(transaction.ins[input.vout].hash).toString('hex') + var prevVout = transaction.ins[input.vout].index + + tasks.push(function(callback) { + blockchain.transactions.get(prevOutTxId, function(err, result) { + if (err) return callback(err) + + var prevOut = bitcoin.Transaction.fromHex(result.txHex) + var prevOutScript = prevOut.outs[prevVout].script + + var scriptSignature = bitcoin.ECSignature.parseScriptSignature(script.chunks[0]) + var publicKey = bitcoin.ECPubKey.fromBuffer(script.chunks[1]) + + var m = transaction.hashForSignature(input.vout, prevOutScript, scriptSignature.hashType) + assert(publicKey.verify(m, scriptSignature.signature), 'Invalid m') + + // store the required information + input.signature = scriptSignature.signature + input.z = bigi.fromBuffer(m) + + return callback() + }) + }) + }) + + // finally, run the tasks, then on to the math + async.parallel(tasks, function(err) { + if (err) throw err + var n = bitcoin.ECKey.curve.n + + for (var i = 0; i < inputs.length; ++i) { + for (var j = i + 1; j < inputs.length; ++j) { + var inputA = inputs[i] + var inputB = inputs[j] + + // enforce matching r values + assert.equal(inputA.signature.r.toString(), inputB.signature.r.toString()) + var r = inputA.signature.r + var rInv = r.modInverse(n) + + var s1 = inputA.signature.s + var s2 = inputB.signature.s + var z1 = inputA.z + var z2 = inputB.z + + var zz = z1.subtract(z2).mod(n) + var ss = s1.subtract(s2).mod(n) + + // k = (z1 - z2) / (s1 - s2) + // d1 = (s1 * k - z1) / r + // d2 = (s2 * k - z2) / r + var k = zz.multiply(ss.modInverse(n)).mod(n) + var d1 = (( s1.multiply(k).mod(n) ).subtract(z1).mod(n) ).multiply(rInv).mod(n) + var d2 = (( s2.multiply(k).mod(n) ).subtract(z2).mod(n) ).multiply(rInv).mod(n) + + // enforce matching private keys + assert.equal(d1.toString(), d2.toString()) + } + } + }) + }) + }) })