Skip to content

Commit 7943417

Browse files
committed
Update everything regarding new flow
1 parent 24e4c25 commit 7943417

9 files changed

+265
-76
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
2-
.idea
2+
.idea
3+
examples/config.js

bip70.md

+60-2
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,63 @@ Since RBF payments can be modified after they are broadcast, they will also be r
99
* `required_fee_rate` - The minimum fee per byte required on your transaction. Bitcoin Cash payments will be rejected if fee rate included for the transaction is not at least this value. _May be fractional value_ ie 0.123 sat/byte
1010

1111
## Application Logic
12-
Please note that you should **NOT** broadcast a payment to the P2P network if we respond with an http status code other than `200`. Broadcasting a payment before getting a success notification back from the server will
13-
lead to a failed payment for the sender. The sender will bear the cost of paying transaction fees yet again to get their money back.
12+
13+
Since rejecting invalid payments before they are broadcast to the network is a primary goal of payment protocol, we recommend following this
14+
flow when submitting a payment.
15+
16+
1. Fetch payment from url (standard BIP 70)
17+
2. Create unsigned, funded transaction (standard BIP 70)
18+
3. Sign transaction, keep unsigned transaction (**bitpay specific**)
19+
4. Send the unsigned transaction and weighed size of the signed transaction to the server (**bitpay specific**)
20+
5. If server rejects at this point, do not continue. Otherwise, send via standard payment protocol and broadcast to p2p in parallel
21+
22+
Please note that you should **NOT** broadcast a payment to the P2P network if we respond with an http status code other than `200` in
23+
the verification step. Broadcasting a payment before getting a success notification back from the server will lead to a failed payment
24+
for the sender. The sender will bear the cost of paying transaction fees yet again to get their money back.
25+
26+
## Payment Request
27+
28+
### Request
29+
A GET request should be made to the payment protocol url.
30+
31+
### Response
32+
The response will payload identical to the BIP70 format with one additional field for `required_fee_rate`.
33+
34+
#### Headers
35+
On a successful request, the response will contain the standard BIP-70 headers.
36+
37+
38+
## Payment Verification
39+
40+
### Request
41+
A POST request should be made to the payment protocol url with the header `application/bitcoin-verify-payment` or `application/bitcoincash-verify-payment`.
42+
43+
### Request Body
44+
The body should contain the **unsigned** transaction as well as the weighted size in vbytes of the fully signed transaction. Weighted size
45+
really only applies for transactions with segwit inputs, if you have a non-segwit transaction the byte size is the correct value to send.
46+
With bitcoin core this is simply the vsize value of a transaction. If you're not certain about calculating the weighted size, please see
47+
the [bitcoin documentation about it](https://en.bitcoin.it/wiki/Weight_units). The format of this request should be based on this protobuf
48+
proto:
49+
```
50+
message PaymentVerification {
51+
required bytes unsigned_transaction = 1;
52+
required uint64 weighted_size = 2 [default = 0];
53+
}
54+
```
55+
56+
### Response
57+
A 200 status code will be returned for valid payments, all other status codes will return with an error message stating why the payment was rejected.
58+
59+
60+
## Payment
61+
62+
### Request
63+
A POST request should be made to the payment protocol url with the standard BIP-70 payment header (`application/bitcoin-payment` or `application/bitcoincash-payment`)
64+
65+
### Request Body
66+
The body should contain the **signed** transaction in BIP-70 format
67+
68+
### Response
69+
A 200 status code will be returned for valid payments, all other status codes will return with an error message stating why the payment was rejected.
70+
71+

examples/bitcoinRpc.js

+7-30
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,12 @@ const promptly = require('promptly');
66
const JsonPaymentProtocol = require('../index'); //or require('json-payment-protocol')
77
const paymentProtocol = new JsonPaymentProtocol({strictSSL: false});
88

9-
let config = {
10-
network: 'test',
11-
currency: 'BTC',
12-
rpcServer: {
13-
username: 'fakeUser',
14-
password: 'fakePassword',
15-
ipAddress: '127.0.0.1',
16-
port: '18332'
17-
},
18-
trustedKeys: {
19-
// The idea is that you or the wallet provider will populate this with keys that are trusted, we have provided a few possible approaches
20-
// in the specification.md document within the 'key-storing suggestions' section
9+
let config;
2110

22-
// Each key here is the pubkey hash so that we can do quick look-ups using the x-identity header sent in the payment request
23-
'mh65MN7drqmwpCRZcEeBEE9ceQCQ95HtZc': {
24-
// This is displayed to the user, somewhat like the organization field on an SSL certificate
25-
owner: 'BitPay (TESTNET ONLY - DO NOT TRUST FOR ACTUAL BITCOIN)',
26-
// Which bitcoin networks is this key valid for (regtest, test, main)
27-
networks: ['test'],
28-
// Which domains this key is valid for
29-
domains: ['test.bitpay.com'],
30-
// The actual public key which should be used to validate the signatures
31-
publicKey: '03159069584176096f1c89763488b94dbc8d5e1fa7bf91f50b42f4befe4e45295a',
32-
}
33-
}
34-
};
35-
36-
if (config.rpcServer.username === 'fakeUser') {
37-
return console.log('You should update the config in this file to match the actual configuration of your bitcoind' +
38-
' RPC interface');
11+
try {
12+
config = require('./config');
13+
} catch(e) {
14+
return console.log('You need to create a config.js file in examples based on the config.example.js file');
3915
}
4016

4117
/**
@@ -200,6 +176,7 @@ async.waterfall([
200176
console.log('Bitcoind did not decode the transaction');
201177
return cb(new Error('Missing decoded tx'));
202178
}
179+
203180
// `vsize` for bitcoin core w/ segwit support, `size` for other clients
204181
let signedTransactionSize = decodedTransaction.vsize || decodedTransaction.size;
205182
cb(null, fundedRawTransaction, signedRawTransaction, signedTransactionSize);
@@ -250,7 +227,7 @@ async.waterfall([
250227
function broadcastPayment(signedRawTransaction, cb) {
251228
async.parallel([
252229
function sendToServer(cb) {
253-
paymentProtocol.broadcastPayment(config.currency, signedRawTransaction, paymentUrl, function(err) {
230+
paymentProtocol.sendSignedPayment(config.currency, signedRawTransaction, paymentUrl, function(err) {
254231
if (err) {
255232
console.log('Error sending payment to server', err);
256233
return cb(err);

examples/config.example.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module.exports = {
2+
network: 'test',
3+
currency: 'BTC',
4+
rpcServer: {
5+
// should set this to match your own bitcoin rpc settings
6+
username: 'fakeUser',
7+
password: 'fakePassword',
8+
ipAddress: '127.0.0.1',
9+
port: '18332'
10+
},
11+
trustedKeys: {
12+
// The idea is that you or the wallet provider will populate this with keys that are trusted, we have provided a few possible approaches
13+
// in the specification.md document within the 'key-storing suggestions' section
14+
15+
// Each key here is the pubkey hash so that we can do quick look-ups using the x-identity header sent in the payment request
16+
'mh65MN7drqmwpCRZcEeBEE9ceQCQ95HtZc': {
17+
// This is displayed to the user, somewhat like the organization field on an SSL certificate
18+
owner: 'BitPay (TESTNET ONLY - DO NOT TRUST FOR ACTUAL BITCOIN)',
19+
// Which bitcoin networks is this key valid for (regtest, test, main)
20+
networks: ['test'],
21+
// Which domains this key is valid for
22+
domains: ['test.bitpay.com'],
23+
// The actual public key which should be used to validate the signatures
24+
publicKey: '03159069584176096f1c89763488b94dbc8d5e1fa7bf91f50b42f4befe4e45295a',
25+
}
26+
}
27+
};

index.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ PaymentProtocol.prototype.parsePaymentRequestAsync = util.promisify(PaymentProto
196196
* Sends a given payment to the server for validation
197197
* @param currency {string} Three letter currency code of proposed transaction (ie BTC, BCH)
198198
* @param unsignedRawTransaction {string} Hexadecimal format raw unsigned transaction
199-
* @param weightedSize {number} Weighted size of the transaction
199+
* @param weightedSize {number} Weighted size of the transaction in bytes
200200
* @param url {string} the payment protocol specific url (https)
201201
* @param callback {function} (err, response)
202202
*/
@@ -221,7 +221,7 @@ PaymentProtocol.prototype.sendPaymentForVerification = function sendPayment(curr
221221
},
222222
body: JSON.stringify({
223223
currency: currency,
224-
transactions: [unsignedRawTransaction],
224+
unsignedTransaction: unsignedRawTransaction,
225225
weightedSize: weightedSize
226226
})
227227
});
@@ -249,7 +249,7 @@ PaymentProtocol.prototype.sendPaymentForVerification = function sendPayment(curr
249249
* Sends a given payment to the server for validation
250250
* @param currency {string} Three letter currency code of proposed transaction (ie BTC, BCH)
251251
* @param unsignedRawTransaction {string} Hexadecimal format raw unsigned transaction
252-
* @param weightedSize {number} Weighted size of the transaction
252+
* @param weightedSize {number} Weighted size of the transaction in bytes
253253
* @param url {string} the payment protocol specific url (https)
254254
*/
255255
PaymentProtocol.prototype.sendPaymentForVerificationAsync = util.promisify(PaymentProtocol.prototype.sendPaymentForVerification);
@@ -261,7 +261,7 @@ PaymentProtocol.prototype.sendPaymentForVerificationAsync = util.promisify(Payme
261261
* @param url {string} the payment protocol specific url (https)
262262
* @param callback {function} (err, response)
263263
*/
264-
PaymentProtocol.prototype.broadcastPayment = function broadcastPayment(currency, signedRawTransaction, url, callback) {
264+
PaymentProtocol.prototype.sendSignedPayment = function sendSignedPayment(currency, signedRawTransaction, url, callback) {
265265
let paymentResponse;
266266

267267
//Basic sanity checks
@@ -303,12 +303,12 @@ PaymentProtocol.prototype.broadcastPayment = function broadcastPayment(currency,
303303
};
304304

305305
/**
306-
* Sends a given payment to the server for validation
306+
* Sends actual payment to server
307307
* @param currency {string} Three letter currency code of proposed transaction (ie BTC, BCH)
308308
* @param signedRawTransaction {string} Hexadecimal format raw signed transaction
309309
* @param url {string} the payment protocol specific url (https)
310310
*/
311-
PaymentProtocol.prototype.broadcastPaymentAsync = util.promisify(PaymentProtocol.prototype.broadcastPayment);
311+
PaymentProtocol.prototype.sendSignedPaymentAsync = util.promisify(PaymentProtocol.prototype.sendSignedPayment);
312312

313313
module.exports = PaymentProtocol;
314314

paymentFlow.png

114 KB
Loading

readme.md

+51-31
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,28 @@ paymentProtocol.getRawPaymentRequest(requestUrl, function (err, response) {
3030
console.log(paymentRequest);
3131

3232
//TODO: Create the rawTransaction and sign it in your wallet instead of this, do NOT broadcast yet
33-
let currency = 'BTC'
34-
let signedRawTransaction = '02000000010c2b0d60448d5cdfebe222014407bdb408b8427f837447484911efddea700323000000006a47304402201d3ed3117f1968c3b0a078f15f8462408c745ff555b173eff3dfe0a25e063c0c02200551572ec33d45ece8e64275970bd1b1694621f0ed8fac2f7e18095f170fe3fe012102d4edb773e3bd94e1251790f5cc543cbfa76c2b0abad14898674b1c4e27176ef2ffffffff02c44e0100000000001976a914dd826377dcf2075e5065713453cfad675ba9434f88aca070002a010000001976a914e7d0344ba970301e93cd7b505c7ae1b5bcf5639288ac00000000';
35-
36-
paymentProtocol.sendPayment(currency, signedRawTransaction, paymentRequest.paymentUrl, function(err, response) {
33+
let currency = 'BTC';
34+
35+
// Funded unsigned raw transaction
36+
let unsignedRawTransaction = '02000000016b7bceefa3ff3bf6f3ad39a99cf6def9126a6edf8f49462bd06e4cb74366dab00100000000feffffff0248590095000000001976a9141b4f4e0c5354ce950ea702cc79be34885e7a60af88ac0c430100000000001976a914072053b485736e002f665d5fc65c443fb379256e88ac00000000'
37+
// Signed version of that transaction
38+
let signedRawTransaction = '02000000016b7bceefa3ff3bf6f3ad39a99cf6def9126a6edf8f49462bd06e4cb74366dab0010000006b4830450221008d8852576eb8e505832a53569dd756a1d0c304606c27e81d0ac1a83e78250969022058b2bde3f2e1ea7e6a62e69d99f7219e846f04c1c58ff163e2996669a935c31501210206e855c3cfd24a5e154cf94ff7a214d598dfc2d62966011fd83c360cf229777ffeffffff0248590095000000001976a9141b4f4e0c5354ce950ea702cc79be34885e7a60af88ac0c430100000000001976a914072053b485736e002f665d5fc65c443fb379256e88ac00000000';
39+
// total size of the signed transaction (note the way shown here is incorrect for segwit, see the code in /examples for getting vsize from RPC)
40+
let signedRawTransactionSize = Buffer.from(signedRawTransaction, 'hex').byteLength;
41+
42+
paymentProtocol.sendPaymentForVerification(currency, unsignedRawTransaction, signedRawTransactionSize, paymentRequest.paymentUrl, function(err, response) {
3743
if (err) {
38-
//DO NOT BROADCAST PAYMENT
39-
return console.log('Error sending payment to server');
44+
// If server rejects, stop, don't broadcast, show user the error
45+
return console.log('Error verifying payment with server', err);
4046
}
41-
console.log('Payment sent successfully');
42-
//TODO: Broadcast payment to network here
47+
48+
// Execute these in parallel
49+
// Sending payment to server via payment protocol
50+
paymentProtocol.broadcastPayment(currency, signedRawTransaction, paymentRequest.paymentUrl, function(err, response) {
51+
console.log('Ignore any errors here if you already received verified above');
52+
});
53+
// Sending payment to bitcoin p2p network
54+
myWallet.broadcastp2p(signedRawTransaction);
4355
});
4456
});
4557
});
@@ -51,29 +63,37 @@ const JsonPaymentProtocol = require('json-payment-protocol');
5163
const paymentProtocol = new JsonPaymentProtocol();
5264

5365
let requestUrl = 'bitcoin:?r=https://test.bitpay.com/i/Jr629pwsXKdTCneLyZja4t';
54-
paymentProtocol
55-
.getRawPaymentRequestAsync(requestUrl)
56-
.then((response) => {
57-
return paymentProtocol.parsePaymentRequestAsync(response.rawBody, response.headers);
58-
})
59-
.then((paymentRequest) => {
60-
console.log('Payment request retrieved');
61-
console.log(paymentRequest);
62-
63-
//TODO: Create the rawTransaction and sign it in your wallet instead of this, do NOT broadcast yet
64-
let currency = 'BTC'
65-
let signedRawTransaction = '02000000010c2b0d60448d5cdfebe222014407bdb408b8427f837447484911efddea700323000000006a47304402201d3ed3117f1968c3b0a078f15f8462408c745ff555b173eff3dfe0a25e063c0c02200551572ec33d45ece8e64275970bd1b1694621f0ed8fac2f7e18095f170fe3fe012102d4edb773e3bd94e1251790f5cc543cbfa76c2b0abad14898674b1c4e27176ef2ffffffff02c44e0100000000001976a914dd826377dcf2075e5065713453cfad675ba9434f88aca070002a010000001976a914e7d0344ba970301e93cd7b505c7ae1b5bcf5639288ac00000000';
66-
67-
return paymentProtocol.sendPaymentAsync(currency, signedRawTransaction, paymentRequest.paymentUrl);
68-
})
69-
.then((response) => {
70-
console.log('Payment sent successfully');
71-
//TODO: Broadcast payment to network here
72-
})
73-
.catch((err) => {
74-
//DO NOT BROADCAST PAYMENT
75-
return console.log('Error processing payment request', err);
76-
});
66+
let response = await paymentProtocol.getRawPaymentRequestAsync(requestUrl);
67+
let paymentRequest = await paymentProtocol.parsePaymentRequestAsync(response.rawBody, response.headers);
68+
69+
console.log('Payment request retrieved');
70+
console.log(paymentRequest);
71+
72+
//TODO: Create the rawTransaction and sign it in your wallet instead of this example, do NOT broadcast yet
73+
// Funded unsigned raw transaction
74+
let unsignedRawTransaction = '02000000016b7bceefa3ff3bf6f3ad39a99cf6def9126a6edf8f49462bd06e4cb74366dab00100000000feffffff0248590095000000001976a9141b4f4e0c5354ce950ea702cc79be34885e7a60af88ac0c430100000000001976a914072053b485736e002f665d5fc65c443fb379256e88ac00000000'
75+
// Signed version of that transaction
76+
let signedRawTransaction = '02000000016b7bceefa3ff3bf6f3ad39a99cf6def9126a6edf8f49462bd06e4cb74366dab0010000006b4830450221008d8852576eb8e505832a53569dd756a1d0c304606c27e81d0ac1a83e78250969022058b2bde3f2e1ea7e6a62e69d99f7219e846f04c1c58ff163e2996669a935c31501210206e855c3cfd24a5e154cf94ff7a214d598dfc2d62966011fd83c360cf229777ffeffffff0248590095000000001976a9141b4f4e0c5354ce950ea702cc79be34885e7a60af88ac0c430100000000001976a914072053b485736e002f665d5fc65c443fb379256e88ac00000000';
77+
// total size of the signed transaction (note the way shown here is incorrect for segwit, see the code in /examples for getting vsize from RPC)
78+
let signedRawTransactionSize = Buffer.from(signedRawTransaction, 'hex').byteLength;
79+
80+
// This sends the proposed unsigned transaction to the server
81+
try {
82+
await paymentProtocol.sendPaymentForVerificationAsync(currency, unsignedRawTransaction, signedRawTransactionSize, paymentRequest.paymentUrl);
83+
} catch (e) {
84+
// Payment was rejected, do not continue, tell the user why they were rejected
85+
return console.log('Proposed payment rejected', e);
86+
}
87+
88+
// This sends the fully signed transaction
89+
try {
90+
await paymentProtocol.sendSignedPaymentAsync(currency, signedRawTransaction, paymentRequest.paymentUrl);
91+
} catch (e) {
92+
// ignore errors here
93+
}
94+
95+
// Broadcast from your wallet to p2p
96+
myWallet.broadcastp2p(signedRawTransaction);
7797
```
7898

7999
### Options

securityUpdates.md

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Security Updates
2+
3+
### 2019-02-01
4+
Recently a bitcoin community member discussed a potential bug in our modified BIP-70 and JSON payment protocol flows when implemented by a
5+
malicious server. This bug would not affect BitPay itself, but has the potential to affect wallets using our payment process
6+
recommendations. This was due to an implicit trust by the client that the server is not malicious.
7+
8+
A proposed malicious flow is as follows:
9+
10+
1. In person Eve asks Adam if she can buy his bitcoin, hands him cash provides a payment protocol url
11+
2. Adams wallet interacts with the url, and sends signed transaction to the server for verification
12+
3. Server rejects signed transaction, but secretly stores it
13+
4. Wallet notifies Adam that the transaction was rejected
14+
5. Eve asks for her money back, Adam complies since transaction was rejected
15+
6. Later Eve has the server broadcast the signed transaction, Eve now has both the cash and the crypto
16+
17+
To resolve this we're advising a change to the payment protocol flow to protect users.
18+
19+
#### Existing flow:
20+
21+
1. Wallet requests payment data
22+
2. Wallet creates unsigned transaction
23+
2. Wallet signs transaction
24+
3. Wallet sends **SIGNED** transaction to server for verification
25+
4. Server verifies or rejects payment and notifies wallet
26+
5. Wallet broadcasts if server accepts, stops if rejected
27+
28+
#### Updated flow:
29+
30+
1. Wallet requests payment data
31+
2. Wallet creates unsigned transaction
32+
3. Wallet signs transaction
33+
4. Wallet sends **UNSIGNED** transaction and weighted size of signed transaction to server for verification
34+
5. Server verifies or rejects payment and notifies wallet
35+
6. Wallet sends **SIGNED** transaction to server and broadcasts to p2p at the same time
36+
37+
This new flow prevents a malicious server from being able to broadcast without the user's knowledge as it now only ever has access to the
38+
unsigned transaction before accepting or rejecting. This is also why we recommend broadcasting to the p2p network at the same time as the
39+
sending the payment via payment protocol to the server. Since you've already gotten approval you should be free to broadcast and ignore
40+
any rejections by the server.
41+
42+
While JSON payment protocol is vulnerable, trying to use a malicious server is less viable than the modified BIP-70 flow due to the need
43+
for each wallet to whitelist ECC keys. Since BIP-70 only requires a valid x509 certificate, anyone with a domain could run a malicious
44+
server. If your wallet uses the modified BIP-70 flow you should update as soon as possible.
45+
46+
Our servers will remain backwards compatible with the old flow for the interim, however to protect users we highly recommend wallets update
47+
to use the more secure flow.
48+
49+
More details of *what* exactly needs to be sent *where* can be found in the updated specification document.

0 commit comments

Comments
 (0)