import paymentrequests only when really needed
[electrum-nvc.git] / lib / paymentrequest.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2014 Thomas Voegtlin
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19
20 import hashlib
21 import httplib
22 import os.path
23 import re
24 import sys
25 import threading
26 import time
27 import traceback
28 import urllib2
29 import urlparse
30
31
32 try:
33     import paymentrequest_pb2
34 except:
35     sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto'")
36
37 try:
38     import requests
39 except ImportError:
40     sys.exit("Error: requests does not seem to be installed. Try 'sudo pip install requests'")
41
42
43 import bitcoin
44 import util
45 import transaction
46 import x509
47
48
49 REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
50 ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
51
52
53 ca_list = {}
54 ca_path = os.path.expanduser("~/.electrum/ca/ca-bundle.crt")
55
56
57
58
59 def load_certificates():
60     try:
61         ca_f = open(ca_path, 'r')
62     except Exception:
63         print "ERROR: Could not open %s"%ca_path
64         print "ca-bundle.crt file should be placed in ~/.electrum/ca/ca-bundle.crt"
65         print "Documentation on how to download or create the file here: http://curl.haxx.se/docs/caextract.html"
66         print "Payment will continue with manual verification."
67         return False
68     c = ""
69     for line in ca_f:
70         if line == "-----BEGIN CERTIFICATE-----\n":
71             c = line
72         else:
73             c += line
74         if line == "-----END CERTIFICATE-----\n":
75             x = x509.X509()
76             try:
77                 x.parse(c)
78             except Exception as e:
79                 util.print_error("cannot parse cert:", e)
80             ca_list[x.getFingerprint()] = x
81     ca_f.close()
82     util.print_error("%d certificates"%len(ca_list))
83     return True
84
85 load_certificates()
86
87
88
89 class PaymentRequest:
90     def __init__(self, config):
91         self.config = config
92         self.outputs = []
93         self.error = ""
94         self.dir_path = os.path.join( self.config.path, 'requests')
95         if not os.path.exists(self.dir_path):
96             os.mkdir(self.dir_path)
97
98     def read(self, url):
99         self.url = url
100         u = urlparse.urlparse(url)
101         self.domain = u.netloc
102         try:
103             connection = httplib.HTTPConnection(u.netloc) if u.scheme == 'http' else httplib.HTTPSConnection(u.netloc)
104             connection.request("GET",u.geturl(), headers=REQUEST_HEADERS)
105             response = connection.getresponse()
106         except:
107             self.error = "cannot read url"
108             return
109
110         try:
111             r = response.read()
112         except:
113             self.error = "cannot read"
114             return
115
116         self.id = bitcoin.sha256(r)[0:16].encode('hex')
117         filename = os.path.join(self.dir_path, self.id)
118         with open(filename,'w') as f:
119             f.write(r)
120
121         return self.parse(r)
122
123
124     def get_status(self):
125         if self.error:
126             return self.error
127         else:
128             return self.status
129
130
131     def read_file(self, key):
132         filename = os.path.join(self.dir_path, key)
133         with open(filename,'r') as f:
134             r = f.read()
135
136         assert key == bitcoin.sha256(r)[0:16].encode('hex')
137         self.id = key
138         self.parse(r)
139
140
141     def parse(self, r):
142         try:
143             self.data = paymentrequest_pb2.PaymentRequest()
144             self.data.ParseFromString(r)
145         except:
146             self.error = "cannot parse payment request"
147             return
148
149
150     def verify(self):
151
152         if not ca_list:
153             self.error = "Trusted certificate authorities list not found"
154             return False
155
156         paymntreq = self.data
157         if not paymntreq.signature:
158             self.error = "No signature"
159             return 
160
161         cert = paymentrequest_pb2.X509Certificates()
162         cert.ParseFromString(paymntreq.pki_data)
163         cert_num = len(cert.certificate)
164
165         x509_chain = []
166         for i in range(cert_num):
167             x = x509.X509()
168             x.parseBinary(bytearray(cert.certificate[i]))
169             x.slow_parse()
170             x509_chain.append(x)
171             if i == 0:
172                 if not x.check_name(self.domain):
173                     self.error = "Certificate Domain Mismatch"
174                     return
175             else:
176                 if not x.check_ca():
177                     self.error = "ERROR: Supplied CA Certificate Error"
178                     return
179
180         if not cert_num > 1:
181             self.error = "ERROR: CA Certificate Chain Not Provided by Payment Processor"
182             return False
183
184         for i in range(1, cert_num):
185             x = x509_chain[i]
186             prev_x = x509_chain[i-1]
187
188             algo, sig, data = prev_x.extract_sig()
189             if algo.getComponentByName('algorithm') != x509.ALGO_RSA_SHA1:
190                 self.error = "Algorithm not suported"
191                 return
192
193             sig = bytearray(sig[5:])
194             pubkey = x.publicKey
195             verify = pubkey.hashAndVerify(sig, data)
196             if not verify:
197                 self.error = "Certificate not Signed by Provided CA Certificate Chain"
198                 return
199
200         ca = x509_chain[cert_num-1]
201         supplied_CA_fingerprint = ca.getFingerprint()
202         supplied_CA_names = ca.extract_names()
203         CA_OU = supplied_CA_names['OU']
204
205         x = ca_list.get(supplied_CA_fingerprint)
206         if x:
207             x.slow_parse()
208             names = x.extract_names()
209             CA_match = True
210             if names['CN'] != supplied_CA_names['CN']:
211                 print "ERROR: Trusted CA CN Mismatch; however CA has trusted fingerprint"
212                 print "Payment will continue with manual verification."
213         else:
214             CA_match = False
215
216         pubkey0 = x509_chain[0].publicKey
217         sig = paymntreq.signature
218         paymntreq.signature = ''
219         s = paymntreq.SerializeToString()
220         sigBytes = bytearray(sig)
221         msgBytes = bytearray(s)
222
223         if paymntreq.pki_type == "x509+sha256":
224             hashBytes = bytearray(hashlib.sha256(msgBytes).digest())
225             prefixBytes = bytearray([0x30,0x31,0x30,0x0d,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01,0x05,0x00,0x04,0x20])
226             verify = pubkey0.verify(sigBytes, prefixBytes + hashBytes)
227         elif paymntreq.pki_type == "x509+sha1":
228             verify = pubkey0.hashAndVerify(sigBytes, msgBytes)
229         else:
230             self.error = "ERROR: Unsupported PKI Type for Message Signature"
231             return False
232
233         if not verify:
234             self.error = "ERROR: Invalid Signature for Payment Request Data"
235             return False
236
237         ### SIG Verified
238         self.details = pay_det = paymentrequest_pb2.PaymentDetails()
239         self.details.ParseFromString(paymntreq.serialized_payment_details)
240
241         for o in pay_det.outputs:
242             addr = transaction.get_address_from_output_script(o.script)[1]
243             self.outputs.append( (addr, o.amount) )
244
245         self.memo = self.details.memo
246
247         if CA_match:
248             self.status = 'Signed by Trusted CA:\n' + CA_OU
249         else:
250             self.status = "Supplied CA Not Found in Trusted CA Store."
251
252         self.payment_url = self.details.payment_url
253
254         if self.has_expired():
255             self.error = "ERROR: Payment Request has Expired."
256             return False
257
258         return True
259
260     def has_expired(self):
261         return self.details.expires and self.details.expires < int(time.time())
262
263     def get_amount(self):
264         return sum(map(lambda x:x[1], self.outputs))
265
266     def get_domain(self):
267         return self.domain
268
269     def get_memo(self):
270         return self.memo
271
272     def get_id(self):
273         return self.id
274
275     def get_outputs(self):
276         return self.outputs[:]
277
278     def send_ack(self, raw_tx, refund_addr):
279
280         pay_det = self.details
281         if not self.details.payment_url:
282             return False, "no url"
283
284         paymnt = paymentrequest_pb2.Payment()
285         paymnt.merchant_data = pay_det.merchant_data
286         paymnt.transactions.append(raw_tx)
287
288         ref_out = paymnt.refund_to.add()
289         ref_out.script = transaction.Transaction.pay_script(refund_addr)
290         paymnt.memo = "Paid using Electrum"
291         pm = paymnt.SerializeToString()
292
293         payurl = urlparse.urlparse(pay_det.payment_url)
294         try:
295             r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path)
296         except requests.exceptions.SSLError:
297             print "Payment Message/PaymentACK verify Failed"
298             try:
299                 r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False)
300             except Exception as e:
301                 print e
302                 return False, "Payment Message/PaymentACK Failed"
303
304         if r.status_code >= 500:
305             return False, r.reason
306
307         try:
308             paymntack = paymentrequest_pb2.PaymentACK()
309             paymntack.ParseFromString(r.content)
310         except Exception:
311             return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received."
312
313         print "PaymentACK message received: %s" % paymntack.memo
314         return True, paymntack.memo
315
316
317
318 if __name__ == "__main__":
319
320     try:
321         uri = sys.argv[1]
322     except:
323         print "usage: %s url"%sys.argv[0]
324         print "example url: \"bitcoin:mpu3yTLdqA1BgGtFUwkVJmhnU3q5afaFkf?r=https%3A%2F%2Fbitcoincore.org%2F%7Egavin%2Ff.php%3Fh%3D2a828c05b8b80dc440c80a5d58890298&amount=1\""
325         sys.exit(1)
326
327     address, amount, label, message, request_url, url = util.parse_url(uri)
328     pr = PaymentRequest(request_url)
329     if not pr.verify():
330         sys.exit(1)
331
332     print 'Payment Request Verified Domain: ', pr.domain
333     print 'outputs', pr.outputs
334     print 'Payment Memo: ', pr.details.memo
335
336     tx = "blah"
337     pr.send_ack(tx, refund_addr = "1vXAXUnGitimzinpXrqDWVU4tyAAQ34RA")
338