diff --git a/lib/api.js b/lib/api.js index 9878a762..4ca7f359 100644 --- a/lib/api.js +++ b/lib/api.js @@ -19,6 +19,10 @@ if (process && !process.browser) { request = require('browser-request'); } +var PayPro = require('bitcore-payment-protocol'); + + +var PayProRequest = require('./payprorequest'); var log = require('./log'); var Credentials = require('./credentials'); var Verifier = require('./verifier'); @@ -42,6 +46,8 @@ function API(opts) { var parsedUrl = url.parse(this.baseUrl); this.basePath = parsedUrl.path; this.baseHost = parsedUrl.protocol + '//' + parsedUrl.host; + this.payProGetter = null; // Only for testing + if (this.verbose) { log.setLevel('debug'); } else { @@ -559,6 +565,27 @@ API.prototype.getStatus = function(cb) { }); }; +API.prototype._computeProposalSignature = function(args) { + $.shouldBeNumber(args.amount); + var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message, args.payProUrl); + return WalletUtils.signMessage(hash, this.credentials.requestPrivKey); +} + +API.prototype.fetchPayPro = function(opts, cb) { + $.checkArgument(opts) + .checkArgument(opts.payProUrl); + + PayProRequest.get({ + url: opts.payProUrl, + getter: this.payProGetter, + }, function(err, paypro) { + if (err) + return cb(err || 'Could not fetch PayPro request'); + + return cb(null, paypro); + }); +}; + /** * Send a transaction proposal * @@ -566,25 +593,23 @@ API.prototype.getStatus = function(cb) { * @param {String} opts.toAddress * @param {Number} opts.amount * @param {String} opts.message + * @param {String} opts.payProUrl Optional: Tx is from a from a payment protocol URL * @returns {Callback} cb - Return error or the transaction proposal */ API.prototype.sendTxProposal = function(opts, cb) { $.checkState(this.credentials && this.credentials.isComplete()); $.checkArgument(opts); - $.shouldBeNumber(opts.amount); - - var self = this; var args = { toAddress: opts.toAddress, amount: opts.amount, - message: API._encryptMessage(opts.message, self.credentials.sharedEncryptingKey), + message: API._encryptMessage(opts.message, this.credentials.sharedEncryptingKey), + payProUrl: opts.payProUrl, }; - var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); - args.proposalSignature = WalletUtils.signMessage(hash, self.credentials.requestPrivKey); - log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); + log.debug('Generating & signing tx proposal:', JSON.stringify(args)); + args.proposalSignature = this._computeProposalSignature(args); - self._doPostRequest('/v1/txproposals/', args, function(err, txp) { + this._doPostRequest('/v1/txproposals/', args, function(err, txp) { if (err) return cb(err); return cb(null, txp); }); @@ -667,27 +692,36 @@ API.prototype.getTxProposals = function(opts, cb) { if (err) return cb(err); API._processTxps(txps, self.credentials.sharedEncryptingKey); - - var fake = _.any(txps, function(txp) { - return (!opts.doNotVerify && !Verifier.checkTxProposal(self.credentials, txp)); - }); - - if (fake) - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - - var result; - if (opts.forAirGapped) { - result = { - txps: JSON.parse(JSON.stringify(txps)), - encryptedPkr: WalletUtils.encryptMessage(JSON.stringify(self.credentials.publicKeyRing), self.credentials.personalEncryptingKey), - m: self.credentials.m, - n: self.credentials.n, - }; - } else { - result = txps; - } - - return cb(null, result); + async.every(txps, + function(txp, acb) { + if (opts.doNotVerify) return acb(true); + + Verifier.checkTxProposal(self.credentials, txp, { + payProGetter: self.payProGetter + }, function(err, isLegit) { + if (err) + return cb(new Error('Cannot check transaction now:' + err)); + + return acb(isLegit); + }); + }, + function(isLegit) { + if (!isLegit) + return cb(new ServerCompromisedError('Server sent fake transaction proposal')); + + var result; + if (opts.forAirGapped) { + result = { + txps: JSON.parse(JSON.stringify(txps)), + encryptedPkr: WalletUtils.encryptMessage(JSON.stringify(self.credentials.publicKeyRing), self.credentials.personalEncryptingKey), + m: self.credentials.m, + n: self.credentials.n, + }; + } else { + result = txps; + } + return cb(null, result); + }); }); }; @@ -707,21 +741,29 @@ API.prototype.signTxProposal = function(txp, cb) { if (!self.canSign() && !txp.signatures) return cb(new Error('You do not have the required keys to sign transactions')); - if (!Verifier.checkTxProposal(self.credentials, txp)) { - return cb(new ServerCompromisedError('Server sent fake transaction proposal')); - } - var signatures = txp.signatures || WalletUtils.signTxp(txp, self.credentials.xPrivKey); + Verifier.checkTxProposal(self.credentials, txp, { + payProGetter: self.payProGetter, + }, function(err, isLegit) { - var url = '/v1/txproposals/' + txp.id + '/signatures/'; - var args = { - signatures: signatures - }; + if (err) + return cb(new Error('Cannot check transaction now:' + err)); - self._doPostRequest(url, args, function(err, txp) { - if (err) return cb(err); - return cb(null, txp); - }); + if (!isLegit) + return cb(new ServerCompromisedError('Server sent fake transaction proposal')); + + var signatures = txp.signatures || WalletUtils.signTxp(txp, self.credentials.xPrivKey); + + var url = '/v1/txproposals/' + txp.id + '/signatures/'; + var args = { + signatures: signatures + }; + + self._doPostRequest(url, args, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }); + }) }; /** @@ -756,9 +798,10 @@ API.prototype.signTxProposalFromAirGapped = function(txp, encryptedPkr, m, n) { self.credentials.n = n; self.credentials.addPublicKeyRing(publicKeyRing); - if (!Verifier.checkTxProposal(self.credentials, txp)) { + // When forAirGapped=true -> checkTxProposal is sync + if (!Verifier.checkTxProposalBody(self.credentials, txp)) throw new Error('Fake transaction proposal'); - } + return WalletUtils.signTxp(txp, self.credentials.xPrivKey); }; diff --git a/lib/payprorequest.js b/lib/payprorequest.js new file mode 100644 index 00000000..1f1dde31 --- /dev/null +++ b/lib/payprorequest.js @@ -0,0 +1,148 @@ +var $ = require('preconditions').singleton(); + +var WalletUtils = require('bitcore-wallet-utils'); +var Bitcore = WalletUtils.Bitcore; +var PayPro = require('bitcore-payment-protocol'); +var PayProRequest = {}; + +PayProRequest._nodeGet = function(opts, cb) { + opts.agent = false; + var http = opts.http || (opts.proto === 'http' ? require("http") : require("https")); + + http.get(opts, function(res) { + if (res.statusCode != 200) + return cb('HTTP Request Error'); + + var data = []; // List of Buffer objects + res.on("data", function(chunk) { + data.push(chunk); // Append Buffer object + }); + res.on("end", function() { + data = Buffer.concat(data); // Make one large Buffer of it + return cb(null, data); + }); + }); +}; + +PayProRequest._browserGet = function(opts, cb) { + var method = (opts.method || 'GET').toUpperCase(); + var url = opts.url; + var req = opts; + + req.headers = req.headers || {}; + req.body = req.body || req.data || ''; + + var xhr = opts.xhr || new XMLHttpRequest(); + xhr.open(method, url, true); + + Object.keys(req.headers).forEach(function(key) { + var val = req.headers[key]; + if (key === 'Content-Length') return; + if (key === 'Content-Transfer-Encoding') return; + xhr.setRequestHeader(key, val); + }); + xhr.responseType = 'arraybuffer'; + + xhr.onload = function(event) { + var response = xhr.response; + return cb(null, new Uint8Array(response)); + }; + + xhr.onerror = function(event) { + var status; + if (xhr.status === 0 || !xhr.statusText) { + status = 'HTTP Request Error'; + } else { + status = xhr.statusText; + } + return cb(status); + }; + + xhr.send(null); +}; + +PayProRequest.get = function(opts, cb) { + $.checkArgument(opts && opts.url); + + var getter = opts.getter; + + opts.headers = opts.headers || { + 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE, + 'Content-Type': 'application/octet-stream', + }; + + if (!opts.getter) { + var env = opts.env; + if (!env) + env = (process && !process.browser) ? 'node' : 'browser'; + + if (env == "node") { + getter = PayProRequest._nodeGet; + } else { + getter = PayProRequest._browserGet; + } + } + + var match = opts.url.match(/^((http[s]?|ftp):\/)?\/?([^:\/\s]+)((\/\w+)*\/)([\w\-\.]+[^#?\s]+)(.*)?(#[\w\-]+)?$/); + + opts.proto = RegExp.$2; + opts.host = RegExp.$3; + opts.path = RegExp.$4 + RegExp.$6; + + getter(opts, function(err, dataBuffer) { + if (err) return cb(err); + var body = PayPro.PaymentRequest.decode(dataBuffer); + var request = (new PayPro()).makePaymentRequest(body); + + var signature = request.get('signature'); + var serializedDetails = request.get('serialized_payment_details'); + + // Verify the signature + var verified = request.verify(true); + + // Get the payment details + var decodedDetails = PayPro.PaymentDetails.decode(serializedDetails); + var pd = new PayPro(); + pd = pd.makePaymentDetails(decodedDetails); + + var outputs = pd.get('outputs'); + if (outputs.length > 1) + return cb(new Error('Payment Protocol Error: Requests with more that one output are not supported')) + + var output = outputs[0]; + + var amount = output.get('amount'); + amount = amount.low + amount.high * 0x100000000; + + + var network = pd.get('network') == 'test' ? 'testnet' : 'livenet'; + + // We love payment protocol + var offset = output.get('script').offset; + var limit = output.get('script').limit; + + // NOTE: For some reason output.script.buffer + // is only an ArrayBuffer + var buffer = new Buffer(new Uint8Array(output.get('script').buffer)); + var scriptBuf = buffer.slice(offset, limit); + var addr = new Bitcore.Address.fromScript(new Bitcore.Script(scriptBuf), network); + + return cb(null, { + verified: verified.verified, + verifyData: { + caName: verified.caName, + selfSigned: verified.selfSigned, + }, + expires: pd.get('expires'), + memo: pd.get('memo'), + time: pd.get('time'), + toAddress: addr.toString(), + amount: amount, + network: network, + domain: opts.host, + url: opts.url, + }); + }); +}; + +module.exports = PayProRequest; diff --git a/lib/verifier.js b/lib/verifier.js index 6b7c1e2d..05babba9 100644 --- a/lib/verifier.js +++ b/lib/verifier.js @@ -7,6 +7,7 @@ var WalletUtils = require('bitcore-wallet-utils'); var Bitcore = WalletUtils.Bitcore; var log = require('./log'); +var PayProRequest = require('./payprorequest'); /** * @desc Verifier constructor. Checks data given by the server @@ -75,14 +76,7 @@ Verifier.checkCopayers = function(credentials, copayers) { return true; }; -/** - * Check transaction proposal - * - * @param {Function} credentials - * @param {Object} txp - * @returns {Boolean} true or false - */ -Verifier.checkTxProposal = function(credentials, txp) { +Verifier.checkTxProposalBody = function(credentials, txp) { $.checkArgument(txp.creatorId); $.checkState(credentials.isComplete()); @@ -94,15 +88,49 @@ Verifier.checkTxProposal = function(credentials, txp) { // TODO: this should be an independent key var creatorSigningPubKey = creatorKeys.requestPubKey; - - var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.encryptedMessage || txp.message); + var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.encryptedMessage || txp.message, txp.payProUrl); log.debug('Regenerating & verifying tx proposal hash -> Hash: ', hash, ' Signature: ', txp.proposalSignature); - if (!WalletUtils.verifyMessage(hash, txp.proposalSignature, creatorSigningPubKey)) return false; - return Verifier.checkAddress(credentials, txp.changeAddress); + if (!Verifier.checkAddress(credentials, txp.changeAddress)) + return false; + + return true; +}; + + + +/** + * Check transaction proposal + * + * @param {Function} credentials + * @param {Object} txp + * @param {Object} Optional: paypro + * @param {Callback} cb(err, isLegit) + */ +Verifier.checkTxProposal = function(credentials, txp, opts, cb) { + if (!this.checkTxProposalBody(credentials, txp)) + return cb(null, false); + + if (txp.payProUrl) { + PayProRequest.get({ + url: txp.payProUrl, + getter: opts.payProGetter, + }, function(err, paypro) { + if (err) + return cb(err || 'Could not fetch PayPro request'); + + var isLegit = false; + if (txp.toAddress == paypro.toAddress && txp.amount == paypro.amount) { + isLegit = true; + } + return cb(null, isLegit); + }); + } else { + return cb(null, true); + } }; module.exports = Verifier; diff --git a/package.json b/package.json index 6f0fbcf4..45989da6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ }, "dependencies": { "async": "^0.9.0", - "bitcore-wallet-utils": "^0.0.5", + "bitcore-payment-protocol": "^0.10.1", + "bitcore-wallet-utils": "^0.0.7", "browser-request": "^0.3.3", "browserify": "^9.0.3", "coveralls": "^2.11.2", @@ -33,13 +34,15 @@ "uglify": "^0.1.1" }, "devDependencies": { - "bitcore-wallet-service": "^0.0.14", + "bitcore-wallet-service": "^0.0.15", "chai": "^1.9.1", - "leveldown": "^0.10.0", - "levelup": "^0.19.0", "grunt-jsdoc": "^0.5.8", + "http": "0.0.0", + "https": "^1.0.0", "istanbul": "*", "jsdoc": "^3.3.0-beta1", + "leveldown": "^0.10.0", + "levelup": "^0.19.0", "memdown": "^1.0.0", "mocha": "^1.18.2", "sinon": "^1.10.3", @@ -51,11 +54,14 @@ "test": "./node_modules/.bin/mocha", "coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, - "contributors": [{ - "name": "Ivan Socolsky", - "email": "ivan@bitpay.com" - }, { - "name": "Matias Alejo Garcia", - "email": "ematiu@gmail.com" - }] + "contributors": [ + { + "name": "Ivan Socolsky", + "email": "ivan@bitpay.com" + }, + { + "name": "Matias Alejo Garcia", + "email": "ematiu@gmail.com" + } + ] } diff --git a/test/client.js b/test/client.js index 12f6ee7c..23793201 100644 --- a/test/client.js +++ b/test/client.js @@ -20,6 +20,7 @@ var Storage = BWS.Storage; var TestData = require('./testdata'); var helpers = {}; +chai.config.includeStack = true; helpers.getRequest = function(app) { $.checkArgument(app); @@ -139,7 +140,7 @@ blockExplorerMock.reset = function() { -describe('client API ', function() { +describe('client API', function() { var clients, app; beforeEach(function() { @@ -680,7 +681,7 @@ describe('client API ', function() { should.not.exist(err); helpers.tamperResponse(clients[0], 'get', '/v1/txproposals/', {}, function(txps) { - txps[0].changeAddress.address = 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5'; + txps[0].changeAddress.address = 'n2tbmpzpecgufct2ebyitj12tpzkhn2mn5'; }, function() { clients[0].getTxProposals({}, function(err, txps) { should.exist(err); @@ -732,6 +733,89 @@ describe('client API ', function() { }); }); + + describe('Payment Protocol', function() { + var getter; + + beforeEach(function(done) { + getter = sinon.stub(); + getter.yields(null, TestData.payProBuf); + helpers.createAndJoinWallet(clients, 2, 2, function(w) { + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + should.exist(x0.address); + blockExplorerMock.setUtxo(x0, 1, 2); + blockExplorerMock.setUtxo(x0, 1, 2); + var opts = { + payProUrl: 'dummy', + }; + clients[0].payProGetter = clients[1].payProGetter = getter; + + clients[0].fetchPayPro(opts, function(err, paypro) { + clients[0].sendTxProposal({ + toAddress: paypro.toAddress, + amount: paypro.amount, + message: paypro.memo, + payProUrl: opts.payProUrl, + }, function(err, x) { + should.not.exist(err); + done(); + }); + }); + }); + }); + }); + + it('Should Create and Verify a Tx from PayPro', function(done) { + + clients[1].getTxProposals({}, function(err, txps) { + should.not.exist(err); + var tx = txps[0]; + // From the hardcoded paypro request + tx.amount.should.equal(404500); + tx.toAddress.should.equal('mjfjcbuYwBUdEyq2m7AezjCAR4etUBqyiE'); + tx.message.should.equal('Payment request for BitPay invoice CibEJJtG1t9H77KmM61E2t for merchant testCopay'); + tx.payProUrl.should.equal('dummy'); + done(); + }); + }); + it('Should Detect tampered PayPro Proposals at getTxProposals', function(done) { + helpers.tamperResponse(clients[1], 'get', '/v1/txproposals/', {}, function(txps) { + txps[0].amount++; + // Generate the right signature (with client 0) + var sig = clients[0]._computeProposalSignature(txps[0]); + txps[0].proposalSignature = sig; + + return txps; + }, function() { + clients[1].getTxProposals({}, function(err, txps) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); + }); + }); + }); + + it('Should Detect tampered PayPro Proposals at signTx', function(done) { + helpers.tamperResponse(clients[1], 'get', '/v1/txproposals/', {}, function(txps) { + txps[0].amount++; + // Generate the right signature (with client 0) + var sig = clients[0]._computeProposalSignature(txps[0]); + txps[0].proposalSignature = sig; + return txps; + }, function() { + clients[1].getTxProposals({ + doNotVerify: true + }, function(err, txps) { + should.not.exist(err); + clients[1].signTxProposal(txps[0], function(err, txps) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); + }); + }); + }); + }); + }); + describe('Transactions Signatures and Rejection', function() { this.timeout(5000); it('Send and broadcast in 1-1 wallet', function(done) { diff --git a/test/payprorequest.js b/test/payprorequest.js new file mode 100644 index 00000000..ec92b3c7 --- /dev/null +++ b/test/payprorequest.js @@ -0,0 +1,129 @@ +'use strict'; + +var _ = require('lodash'); +var chai = chai || require('chai'); +var sinon = sinon || require('sinon'); +var should = chai.should(); +var PayProReq = require('../lib/payprorequest'); +var TestData = require('./testdata'); + + +describe('payprorequest', function() { + var xhr, http; + before(function() { + xhr = {}; + xhr.onCreate = function(req) {}; + xhr.open = function(method, url) {}; + xhr.setRequestHeader = function(k, v) {}; + xhr.getAllResponseHeaders = function() { + return 'content-type: test'; + }; + xhr.send = function() { + xhr.response = TestData.payProBuf; + xhr.onload(); + }; + + http = {}; + http.get = function(opts, cb) { + var res = {}; + res.statusCode = http.error || 200; + res.on = function(e, cb) { + if (e == 'data') + return cb(TestData.payProBuf); + if (e == 'end') + return cb(); + }; + return cb(res); + }; + }); + + it('Make a PP request with browser', function(done) { + PayProReq.get({ + url: 'http://an.url.com/paypro', + xhr: xhr, + env: 'browser', + }, function(err, res) { + should.not.exist(err); + res.should.deep.equal(TestData.payProData); + done(); + }); + }); + + it('Make a PP request with browser with headers', function(done) { + PayProReq.get({ + url: 'http://an.url.com/paypro', + xhr: xhr, + env: 'browser', + headers: { + 'Accept': 'xx/xxx', + 'Content-Type': 'application/octet-stream', + 'Content-Length': 0, + 'Content-Transfer-Encoding': 'xxx', + } + + }, function(err, res) { + should.not.exist(err); + res.should.deep.equal(TestData.payProData); + done(); + }); + }); + + + + it('make a pp request with browser, with http error', function(done) { + xhr.send = function() { + xhr.onerror(); + }; + PayProReq.get({ + url: 'http://an.url.com/paypro', + xhr: xhr, + env: 'browser', + }, function(err, res) { + err.should.contain('HTTP Request Error'); + done(); + }); + }); + + it('Make a PP request with browser, with http given error', function(done) { + xhr.send = function() { + xhr.onerror(); + }; + xhr.statusText = 'myerror'; + PayProReq.get({ + url: 'http://an.url.com/paypro', + xhr: xhr, + env: 'browser', + }, function(err, res) { + err.should.contain('myerror'); + done(); + }); + }); + + + it('Make a PP request with node', function(done) { + PayProReq.get({ + url: 'http://an.url.com/paypro', + http: http, + env: 'node', + }, function(err, res) { + should.not.exist(err); + res.should.deep.equal(TestData.payProData); + done(); + }); + }); + + it('Make a PP request with node with HTTP error', function(done) { + http.error = 404; + PayProReq.get({ + url: 'http://an.url.com/paypro', + http: http, + env: 'node', + }, function(err, res) { + err.should.contain('HTTP Request Error'); + done(); + }); + }); + + + +}); diff --git a/test/testdata.js b/test/testdata.js index 9686ff77..281b3814 100644 --- a/test/testdata.js +++ b/test/testdata.js @@ -73,4 +73,24 @@ var history = [{ fees: 0.00014299 }]; +var payproHex = '0801120b783530392b7368613235361a89250aa40a3082052030820408a003020102020727a49d05046d62300d06092a864886f70d01010b05003081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d204732301e170d3134303432363132333532365a170d3136303432363132333532365a303a3121301f060355040b1318446f6d61696e20436f6e74726f6c2056616c6964617465643115301306035504030c0c2a2e6269747061792e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100e2a5dd4aea959c1d0fb016e6e05bb7011e741cdc61918c61f9625a2f682f485f0e862ea63db61cc9161753127504de800604df36b10f46cb17ab6cb99dba8aa45a36adfb901a2fc380c89e234bce18de6639b883e9339801673efaee1f2df77eeb82f7c39c96a2f8ef4572b634c203d9be8fd1e0036d32fb38b6b9b5ecd5a0684345c7e9ffc5d26bc6fd69aa6619f77badaa4bfb989478fb2f41aa92782e40b34ba9ac4549a4e6fda76b5fc4a581853bd0de5fb5a2c6dfdc12cdfadb54e9636a6d1223705924b8be566b81ac7921078cf590a146ae397a84908ef4fc83ff5715a44ab59e9258674d90113bb607b8d81eb268e4c6ce849497c76521795b0873950203010001a38201ae308201aa300f0603551d130101ff04053003010100301d0603551d250416301406082b0601050507030106082b06010505070302300e0603551d0f0101ff0404030205a030360603551d1f042f302d302ba029a0278625687474703a2f2f63726c2e676f64616464792e636f6d2f676469673273312d34392e63726c30530603551d20044c304a3048060b6086480186fd6d010717013039303706082b06010505070201162b687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f307606082b06010505070101046a3068302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f304006082b060105050730028634687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f67646967322e637274301f0603551d2304183016801440c2bd278ecc348330a233d7fb6cb3f0b42c80ce30230603551d11041c301a820c2a2e6269747061792e636f6d820a6269747061792e636f6d301d0603551d0e0416041485454e3b4072e2f58e377438988b5229387e967a300d06092a864886f70d01010b050003820101002d0a7ef97f988905ebbbad4e9ffb690352535211d6792516119838b55f24ff9fa4e93b6187b8517cbb0477457d3378078ef66057abe41bcafeb142ec52443a94b88114fa069f725c6198581d97af16352727f4f35e7f2110faa41a0511bcfdf8e3f4a3a310278c150b10f32a962c81e8f3d5374d9cb56d893027ff4fa4e3c3e6384c1f1557ceea6fca9cbc0c110748c08b82d8f0ed9a579637ee43a2d8fec3b5b04d1f3c8f1a3e2088da2274b6bc60948bbe744a7f8b942b41f0ae9b4afaeefbb7e0f04a0587b52efb6ebfa2d970b9de56a068575e4bf0cf824618dc17bbeaa2cdd25d65970a9f1a06fc9fffb466a10c9568cd651795bc2c7996975027bdbaba0ad409308204d0308203b8a003020102020107300d06092a864886f70d01010b0500308183310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e3131302f06035504031328476f20446164647920526f6f7420436572746966696361746520417574686f72697479202d204732301e170d3131303530333037303030305a170d3331303530333037303030305a3081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d20473230820122300d06092a864886f70d01010105000382010f003082010a0282010100b9e0cb10d4af76bdd49362eb3064b881086cc304d962178e2fff3e65cf8fce62e63c521cda16454b55ab786b63836290ce0f696c99c81a148b4ccc4533ea88dc9ea3af2bfe80619d7957c4cf2ef43f303c5d47fc9a16bcc3379641518e114b54f828bed08cbef030381ef3b026f86647636dde7126478f384753d1461db4e3dc00ea45acbdbc71d9aa6f00dbdbcd303a794f5f4c47f81def5bc2c49d603bb1b24391d8a4334eeab3d6274fad258aa5c6f4d5d0a6ae7405645788b54455d42d2a3a3ef8b8bde9320a029464c4163a50f14aaee77933af0c20077fe8df0439c269026c6352fa77c11bc87487c8b993185054354b694ebc3bd3492e1fdcc1d252fb0203010001a382011a30820116300f0603551d130101ff040530030101ff300e0603551d0f0101ff040403020106301d0603551d0e0416041440c2bd278ecc348330a233d7fb6cb3f0b42c80ce301f0603551d230418301680143a9a8507106728b6eff6bd05416e20c194da0fde303406082b0601050507010104283026302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f30350603551d1f042e302c302aa028a0268624687474703a2f2f63726c2e676f64616464792e636f6d2f6764726f6f742d67322e63726c30460603551d20043f303d303b0604551d20003033303106082b06010505070201162568747470733a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f300d06092a864886f70d01010b05000382010100087e6c9310c838b896a9904bffa15f4f04ef6c3e9c8806c9508fa673f757311bbebce42fdbf8bad35be0b4e7e679620e0ca2d76a637331b5f5a848a43b082da25d90d7b47c254f115630c4b6449d7b2c9de55ee6ef0c61aabfe42a1bee849eb8837dc143ce44a713700d911ff4c813ad8360d9d872a873241eb5ac220eca17896258441bab892501000fcdc41b62db51b4d30f512a9bf4bc73fc76ce36a4cdd9d82ceaae9bf52ab290d14d75188a3f8a4190237d5b4bfea403589b46b2c3606083f87d5041cec2a190c3bbef022fd21554ee4415d90aaea78a33edb12d763626dc04eb9ff7611f15dc876fee469628ada1267d0a09a72e04a38dbcf8bc0430010a81093082047d30820365a00302010202031be715300d06092a864886f70d01010b05003063310b30090603550406130255533121301f060355040a131854686520476f2044616464792047726f75702c20496e632e3131302f060355040b1328476f20446164647920436c61737320322043657274696669636174696f6e20417574686f72697479301e170d3134303130313037303030305a170d3331303533303037303030305a308183310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e3131302f06035504031328476f20446164647920526f6f7420436572746966696361746520417574686f72697479202d20473230820122300d06092a864886f70d01010105000382010f003082010a0282010100bf716208f1fa5934f71bc918a3f7804958e9228313a6c52043013b84f1e685499f27eaf6841b4ea0b4db7098c73201b1053e074eeef4fa4f2f593022e7ab19566be28007fcf316758039517be5f935b6744ea98d8213e4b63fa90383faa2be8a156a7fde0bc3b6191405caeac3a804943b467c320df3006622c88d696d368c1118b7d3b21c60b438fa028cced3dd4607de0a3eeb5d7cc87cfbb02b53a4926269512505611a44818c2ca9439623dfac3a819a0e29c51ca9e95d1eb69e9e300a39cef18880fb4b5dcc32ec85624325340256270191b43b702a3f6eb1e89c88017d9fd4f9db536d609dbf2ce758abb85f46fccec41b033c09eb49315c6946b3e0470203010001a382011730820113300f0603551d130101ff040530030101ff300e0603551d0f0101ff040403020106301d0603551d0e041604143a9a8507106728b6eff6bd05416e20c194da0fde301f0603551d23041830168014d2c4b0d291d44c1171b361cb3da1fedda86ad4e3303406082b0601050507010104283026302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f30320603551d1f042b30293027a025a0238621687474703a2f2f63726c2e676f64616464792e636f6d2f6764726f6f742e63726c30460603551d20043f303d303b0604551d20003033303106082b06010505070201162568747470733a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f300d06092a864886f70d01010b05000382010100590b53bd928611a7247bed5b31cf1d1f6c70c5b86ebe4ebbf6be9750e1307fba285c6294c2e37e33f7fb427685db951c8c225875090c886567390a1609c5a03897a4c523933fb418a601064491e3a76927b45a257f3ab732cddd84ff2a382933a4dd67b285fea188201c5089c8dc2af64203374ce688dfd5af24f2b1c3dfccb5ece0995eb74954203c94180cc71c521849a46de1b3580bc9d8ecd9ae1c328e28700de2fea6179e840fbd5770b35ae91fa08653bbef7cff690be048c3b7930bc80a54c4ac5d1467376ccaa52f310837aa6e6f8cbc9be2575d2481af97979c84ad6cac374c66f361911120e4be309f7aa42909b0e1345f6477184051df8c30a6af0a840830820400308202e8a003020102020100300d06092a864886f70d01010505003063310b30090603550406130255533121301f060355040a131854686520476f2044616464792047726f75702c20496e632e3131302f060355040b1328476f20446164647920436c61737320322043657274696669636174696f6e20417574686f72697479301e170d3034303632393137303632305a170d3334303632393137303632305a3063310b30090603550406130255533121301f060355040a131854686520476f2044616464792047726f75702c20496e632e3131302f060355040b1328476f20446164647920436c61737320322043657274696669636174696f6e20417574686f7269747930820120300d06092a864886f70d01010105000382010d00308201080282010100de9dd7ea571849a15bebd75f4886eabeddffe4ef671cf46568b35771a05e77bbed9b49e970803d561863086fdaf2ccd03f7f0254225410d8b281d4c0753d4b7fc777c33e78ab1a03b5206b2f6a2bb1c5887ec4bb1eb0c1d845276faa3758f78726d7d82df6a917b71f72364ea6173f659892db2a6e5da2fe88e00bde7fe58d15e1ebcb3ad5e212a2132dd88eaf5f123da0080508b65ca565380445991ea3606074c541a572621b62c51f6f5f1a42be025165a8ae23186afc7803a94d7f80c3faab5afca140a4ca1916feb2c8ef5e730dee77bd9af67998bcb10767a2150ddda058c6447b0a3e62285fba41075358cf117e3874c5f8ffb569908f8474ea971baf020103a381c03081bd301d0603551d0e04160414d2c4b0d291d44c1171b361cb3da1fedda86ad4e330818d0603551d230481853081828014d2c4b0d291d44c1171b361cb3da1fedda86ad4e3a167a4653063310b30090603550406130255533121301f060355040a131854686520476f2044616464792047726f75702c20496e632e3131302f060355040b1328476f20446164647920436c61737320322043657274696669636174696f6e20417574686f72697479820100300c0603551d13040530030101ff300d06092a864886f70d01010505000382010100324bf3b2ca3e91fc12c6a1078c8e77a03306145c901e18f708a63d0a19f98780116e69e4961730ff3491637238eecc1c01a31d9428a431f67ac454d7f6e5315803a2ccce62db944573b5bf45c924b5d58202ad2379698db8b64dcecf4cca3323e81c88aa9d8b416e16c920e5899ecd3bda70f77e992620145425ab6e7385e69b219d0a6c820ea8f8c20cfa101e6c96ef870dc40f618badee832b95f88e92847239eb20ea83ed83cd976e08bceb4e26b6732be4d3f64cfe2671e26111744aff571a870f75482ecf516917a002126195d5d140b2104ceec4ac1043a6a59e0ad595629a0dcf8882c5320ce42b9f45e60d9f289cb1b92a5a57ad370faf1d7fdbbd9f2285020a0474657374121f0894d818121976a9142d89a9720d0aca9beaae478994a06b1ab178186788ac18f3f2caa80520f7f9caa8052a505061796d656e74207265717565737420666f722042697450617920696e766f69636520436962454a4a74473174394837374b6d4d363145327420666f72206d65726368616e742074657374436f706179323068747470733a2f2f746573742e6269747061792e636f6d2f692f436962454a4a74473174394837374b6d4d36314532743a4c7b22696e766f6963654964223a22436962454a4a74473174394837374b6d4d3631453274222c226d65726368616e744964223a22444766754344656f66556e576a446d5537454c634568227d2a8002c7146109dfd2584b905627f13e79fe96cc390de6d9729f1263be9ded44f907cc185a1968b71d1b99f073671e288ff51be93493dc2b0cbd7a9de761692bbb143c117aa24961c64e3add6a35b67b48da73c6c740024665494c28cd80d6bbf99ab98d9cee87a6bf826666990d51791d87978cbefd132679851c19962c0ba364913786ec6c9706989c0b4e257d1313cd635822569babff5e58e41f4f94add69efc5ed2850fc1c87cac0487ef3678d02b92459e04666f0e2d3e530502c0623768cd741262fcdf696817ffecb93917152a16a701d21f0a257302d2596f3c86b3fa296450662a11fdd857c40e6bb50cfad4e65cf647eb65541a617661c69da903c54bf6'; + +var payProData = { + verified: true, + verifyData: { + caName: 'Go Daddy Class 2 CA', + selfSigned: 0 + }, + expires: 1427291383, + memo: 'Payment request for BitPay invoice CibEJJtG1t9H77KmM61E2t for merchant testCopay', + time: 1427290483, + toAddress: 'mjfjcbuYwBUdEyq2m7AezjCAR4etUBqyiE', + amount: 404500, + network: 'testnet', + domain: 'an.url.com', + url: 'http://an.url.com/paypro', +}; + module.exports.history = history; +module.exports.payProBuf = new Buffer(payproHex, 'hex'); +module.exports.payProData = payProData;