column for payment request memo
[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 # status can be:
53 PR_UNPAID  = 0
54 PR_EXPIRED = 1
55 PR_SENT    = 2     # sent but not propagated
56 PR_PAID    = 3     # send and propagated
57 PR_ERROR   = 4     # could not parse
58
59
60 ca_list = {}
61
62
63
64
65 def load_certificates():
66
67     ca_path = os.path.expanduser("~/.electrum/ca/ca-bundle.crt")
68     try:
69         ca_f = open(ca_path, 'r')
70     except Exception:
71         print "ERROR: Could not open %s"%ca_path
72         print "ca-bundle.crt file should be placed in ~/.electrum/ca/ca-bundle.crt"
73         print "Documentation on how to download or create the file here: http://curl.haxx.se/docs/caextract.html"
74         print "Payment will continue with manual verification."
75         return False
76     c = ""
77     for line in ca_f:
78         if line == "-----BEGIN CERTIFICATE-----\n":
79             c = line
80         else:
81             c += line
82         if line == "-----END CERTIFICATE-----\n":
83             x = x509.X509()
84             try:
85                 x.parse(c)
86             except Exception as e:
87                 print "cannot parse cert:", e
88             ca_list[x.getFingerprint()] = x
89     ca_f.close()
90     util.print_error("%d certificates"%len(ca_list))
91     return True
92
93 load_certificates()
94
95
96
97 class PaymentRequest:
98     def __init__(self, config):
99         self.config = config
100         self.outputs = []
101         self.error = ""
102         self.dir_path = os.path.join( self.config.path, 'requests')
103         if not os.path.exists(self.dir_path):
104             os.mkdir(self.dir_path)
105
106     def read(self, url):
107         self.url = url
108         u = urlparse.urlparse(url)
109         self.domain = u.netloc
110         try:
111             connection = httplib.HTTPConnection(u.netloc) if u.scheme == 'http' else httplib.HTTPSConnection(u.netloc)
112             connection.request("GET",u.geturl(), headers=REQUEST_HEADERS)
113             response = connection.getresponse()
114         except:
115             self.error = "cannot read url"
116             return
117
118         try:
119             r = response.read()
120         except:
121             self.error = "cannot read"
122             return
123
124         self.id = bitcoin.sha256(r)[0:16].encode('hex')
125         filename = os.path.join(self.dir_path, self.id)
126         with open(filename,'w') as f:
127             f.write(r)
128
129         return self.parse(r)
130
131
132     def get_status(self):
133         if self.error:
134             return self.error
135         else:
136             return self.status
137
138
139     def read_file(self, key):
140         filename = os.path.join(self.dir_path, key)
141         with open(filename,'r') as f:
142             r = f.read()
143
144         self.parse(r)
145
146
147     def parse(self, r):
148         try:
149             self.data = paymentrequest_pb2.PaymentRequest()
150             self.data.ParseFromString(r)
151         except:
152             self.error = "cannot parse payment request"
153             return
154
155
156     def verify(self):
157
158         if not ca_list:
159             self.error = "Trusted certificate authorities list not found"
160             return False
161
162         paymntreq = self.data
163         if not paymntreq.signature:
164             self.error = "No signature"
165             return 
166
167         cert = paymentrequest_pb2.X509Certificates()
168         cert.ParseFromString(paymntreq.pki_data)
169         cert_num = len(cert.certificate)
170
171         x509_chain = []
172         for i in range(cert_num):
173             x = x509.X509()
174             x.parseBinary(bytearray(cert.certificate[i]))
175             x.slow_parse()
176             x509_chain.append(x)
177             if i == 0:
178                 if not x.check_name(self.domain):
179                     self.error = "Certificate Domain Mismatch"
180                     return
181             else:
182                 if not x.check_ca():
183                     self.error = "ERROR: Supplied CA Certificate Error"
184                     return
185
186         if not cert_num > 1:
187             self.error = "ERROR: CA Certificate Chain Not Provided by Payment Processor"
188             return False
189
190         for i in range(1, cert_num):
191             x = x509_chain[i]
192             prev_x = x509_chain[i-1]
193
194             algo, sig, data = prev_x.extract_sig()
195             if algo.getComponentByName('algorithm') != x509.ALGO_RSA_SHA1:
196                 self.error = "Algorithm not suported"
197                 return
198
199             sig = bytearray(sig[5:])
200             pubkey = x.publicKey
201             verify = pubkey.hashAndVerify(sig, data)
202             if not verify:
203                 self.error = "Certificate not Signed by Provided CA Certificate Chain"
204                 return
205
206         ca = x509_chain[cert_num-1]
207         supplied_CA_fingerprint = ca.getFingerprint()
208         supplied_CA_names = ca.extract_names()
209         CA_OU = supplied_CA_names['OU']
210
211         x = ca_list.get(supplied_CA_fingerprint)
212         if x:
213             x.slow_parse()
214             names = x.extract_names()
215             CA_match = True
216             if names['CN'] != supplied_CA_names['CN']:
217                 print "ERROR: Trusted CA CN Mismatch; however CA has trusted fingerprint"
218                 print "Payment will continue with manual verification."
219         else:
220             CA_match = False
221
222         pubkey0 = x509_chain[0].publicKey
223         sig = paymntreq.signature
224         paymntreq.signature = ''
225         s = paymntreq.SerializeToString()
226         sigBytes = bytearray(sig)
227         msgBytes = bytearray(s)
228
229         if paymntreq.pki_type == "x509+sha256":
230             hashBytes = bytearray(hashlib.sha256(msgBytes).digest())
231             prefixBytes = bytearray([0x30,0x31,0x30,0x0d,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01,0x05,0x00,0x04,0x20])
232             verify = pubkey0.verify(sigBytes, prefixBytes + hashBytes)
233         elif paymntreq.pki_type == "x509+sha1":
234             verify = pubkey0.hashAndVerify(sigBytes, msgBytes)
235         else:
236             self.error = "ERROR: Unsupported PKI Type for Message Signature"
237             return False
238
239         if not verify:
240             self.error = "ERROR: Invalid Signature for Payment Request Data"
241             return False
242
243         ### SIG Verified
244         self.details = pay_det = paymentrequest_pb2.PaymentDetails()
245         self.details.ParseFromString(paymntreq.serialized_payment_details)
246
247         for o in pay_det.outputs:
248             addr = transaction.get_address_from_output_script(o.script)[1]
249             self.outputs.append( (addr, o.amount) )
250
251         self.memo = self.details.memo
252
253         if CA_match:
254             self.status = 'Signed by Trusted CA:\n' + CA_OU
255         else:
256             self.status = "Supplied CA Not Found in Trusted CA Store."
257
258         self.payment_url = self.details.payment_url
259
260         if self.has_expired():
261             self.error = "ERROR: Payment Request has Expired."
262             return False
263
264         return True
265
266     def has_expired(self):
267         return self.details.expires and self.details.expires < int(time.time())
268
269     def get_amount(self):
270         return sum(map(lambda x:x[1], self.outputs))
271
272     def get_domain(self):
273         return self.domain
274
275     def get_memo(self):
276         return self.memo
277
278     def get_id(self):
279         return self.id
280
281     def get_outputs(self):
282         return self.outputs
283
284     def send_ack(self, raw_tx, refund_addr):
285
286         if self.has_expired():
287             return False, "has expired"
288
289         pay_det = self.details
290         if not self.details.payment_url:
291             return False, "no url"
292
293         paymnt = paymentrequest_pb2.Payment()
294         paymnt.merchant_data = pay_det.merchant_data
295         paymnt.transactions.append(raw_tx)
296
297         ref_out = paymnt.refund_to.add()
298         ref_out.script = transaction.Transaction.pay_script(refund_addr)
299         paymnt.memo = "Paid using Electrum"
300         pm = paymnt.SerializeToString()
301
302         payurl = urlparse.urlparse(pay_det.payment_url)
303         try:
304             r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path)
305         except requests.exceptions.SSLError:
306             print "Payment Message/PaymentACK verify Failed"
307             try:
308                 r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False)
309             except Exception as e:
310                 print e
311                 return False, "Payment Message/PaymentACK Failed"
312
313         if r.status_code >= 500:
314             return False, r.reason
315
316         try:
317             paymntack = paymentrequest_pb2.PaymentACK()
318             paymntack.ParseFromString(r.content)
319         except Exception:
320             return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received."
321
322         print "PaymentACK message received: %s" % paymntack.memo
323         return True, paymntack.memo
324
325
326
327 if __name__ == "__main__":
328
329     try:
330         uri = sys.argv[1]
331     except:
332         print "usage: %s url"%sys.argv[0]
333         print "example url: \"bitcoin:mpu3yTLdqA1BgGtFUwkVJmhnU3q5afaFkf?r=https%3A%2F%2Fbitcoincore.org%2F%7Egavin%2Ff.php%3Fh%3D2a828c05b8b80dc440c80a5d58890298&amount=1\""
334         sys.exit(1)
335
336     address, amount, label, message, request_url, url = util.parse_url(uri)
337     pr = PaymentRequest(request_url)
338     if not pr.verify():
339         sys.exit(1)
340
341     print 'Payment Request Verified Domain: ', pr.domain
342     print 'outputs', pr.outputs
343     print 'Payment Memo: ', pr.details.memo
344
345     tx = "blah"
346     pr.send_ack(tx, refund_addr = "1vXAXUnGitimzinpXrqDWVU4tyAAQ34RA")
347