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