b0daa22eb229c130b801d745a30e8ee3eb5e6a32
[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         return True
255
256     def has_expired(self):
257         return self.details.expires and self.details.expires < int(time.time())
258
259     def get_expiration_date(self):
260         return self.details.expires
261
262     def get_amount(self):
263         return sum(map(lambda x:x[1], self.outputs))
264
265     def get_domain(self):
266         return self.domain
267
268     def get_memo(self):
269         return self.memo
270
271     def get_id(self):
272         return self.id
273
274     def get_outputs(self):
275         return self.outputs[:]
276
277     def send_ack(self, raw_tx, refund_addr):
278
279         pay_det = self.details
280         if not self.details.payment_url:
281             return False, "no url"
282
283         paymnt = paymentrequest_pb2.Payment()
284         paymnt.merchant_data = pay_det.merchant_data
285         paymnt.transactions.append(raw_tx)
286
287         ref_out = paymnt.refund_to.add()
288         ref_out.script = transaction.Transaction.pay_script(refund_addr)
289         paymnt.memo = "Paid using Electrum"
290         pm = paymnt.SerializeToString()
291
292         payurl = urlparse.urlparse(pay_det.payment_url)
293         try:
294             r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path)
295         except requests.exceptions.SSLError:
296             print "Payment Message/PaymentACK verify Failed"
297             try:
298                 r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False)
299             except Exception as e:
300                 print e
301                 return False, "Payment Message/PaymentACK Failed"
302
303         if r.status_code >= 500:
304             return False, r.reason
305
306         try:
307             paymntack = paymentrequest_pb2.PaymentACK()
308             paymntack.ParseFromString(r.content)
309         except Exception:
310             return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received."
311
312         print "PaymentACK message received: %s" % paymntack.memo
313         return True, paymntack.memo
314
315
316
317 if __name__ == "__main__":
318
319     try:
320         uri = sys.argv[1]
321     except:
322         print "usage: %s url"%sys.argv[0]
323         print "example url: \"bitcoin:mpu3yTLdqA1BgGtFUwkVJmhnU3q5afaFkf?r=https%3A%2F%2Fbitcoincore.org%2F%7Egavin%2Ff.php%3Fh%3D2a828c05b8b80dc440c80a5d58890298&amount=1\""
324         sys.exit(1)
325
326     address, amount, label, message, request_url, url = util.parse_url(uri)
327     pr = PaymentRequest(request_url)
328     if not pr.verify():
329         sys.exit(1)
330
331     print 'Payment Request Verified Domain: ', pr.domain
332     print 'outputs', pr.outputs
333     print 'Payment Memo: ', pr.details.memo
334
335     tx = "blah"
336     pr.send_ack(tx, refund_addr = "1vXAXUnGitimzinpXrqDWVU4tyAAQ34RA")
337