verify if pr 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 # 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 ca_path = os.path.expanduser("~/.electrum/ca/ca-bundle.crt")
62
63
64
65
66 def load_certificates():
67
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         assert key == bitcoin.sha256(r)[0:16].encode('hex')
145         self.id = key
146         self.parse(r)
147
148
149     def parse(self, r):
150         try:
151             self.data = paymentrequest_pb2.PaymentRequest()
152             self.data.ParseFromString(r)
153         except:
154             self.error = "cannot parse payment request"
155             return
156
157
158     def verify(self):
159
160         if not ca_list:
161             self.error = "Trusted certificate authorities list not found"
162             return False
163
164         paymntreq = self.data
165         if not paymntreq.signature:
166             self.error = "No signature"
167             return 
168
169         cert = paymentrequest_pb2.X509Certificates()
170         cert.ParseFromString(paymntreq.pki_data)
171         cert_num = len(cert.certificate)
172
173         x509_chain = []
174         for i in range(cert_num):
175             x = x509.X509()
176             x.parseBinary(bytearray(cert.certificate[i]))
177             x.slow_parse()
178             x509_chain.append(x)
179             if i == 0:
180                 if not x.check_name(self.domain):
181                     self.error = "Certificate Domain Mismatch"
182                     return
183             else:
184                 if not x.check_ca():
185                     self.error = "ERROR: Supplied CA Certificate Error"
186                     return
187
188         if not cert_num > 1:
189             self.error = "ERROR: CA Certificate Chain Not Provided by Payment Processor"
190             return False
191
192         for i in range(1, cert_num):
193             x = x509_chain[i]
194             prev_x = x509_chain[i-1]
195
196             algo, sig, data = prev_x.extract_sig()
197             if algo.getComponentByName('algorithm') != x509.ALGO_RSA_SHA1:
198                 self.error = "Algorithm not suported"
199                 return
200
201             sig = bytearray(sig[5:])
202             pubkey = x.publicKey
203             verify = pubkey.hashAndVerify(sig, data)
204             if not verify:
205                 self.error = "Certificate not Signed by Provided CA Certificate Chain"
206                 return
207
208         ca = x509_chain[cert_num-1]
209         supplied_CA_fingerprint = ca.getFingerprint()
210         supplied_CA_names = ca.extract_names()
211         CA_OU = supplied_CA_names['OU']
212
213         x = ca_list.get(supplied_CA_fingerprint)
214         if x:
215             x.slow_parse()
216             names = x.extract_names()
217             CA_match = True
218             if names['CN'] != supplied_CA_names['CN']:
219                 print "ERROR: Trusted CA CN Mismatch; however CA has trusted fingerprint"
220                 print "Payment will continue with manual verification."
221         else:
222             CA_match = False
223
224         pubkey0 = x509_chain[0].publicKey
225         sig = paymntreq.signature
226         paymntreq.signature = ''
227         s = paymntreq.SerializeToString()
228         sigBytes = bytearray(sig)
229         msgBytes = bytearray(s)
230
231         if paymntreq.pki_type == "x509+sha256":
232             hashBytes = bytearray(hashlib.sha256(msgBytes).digest())
233             prefixBytes = bytearray([0x30,0x31,0x30,0x0d,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01,0x05,0x00,0x04,0x20])
234             verify = pubkey0.verify(sigBytes, prefixBytes + hashBytes)
235         elif paymntreq.pki_type == "x509+sha1":
236             verify = pubkey0.hashAndVerify(sigBytes, msgBytes)
237         else:
238             self.error = "ERROR: Unsupported PKI Type for Message Signature"
239             return False
240
241         if not verify:
242             self.error = "ERROR: Invalid Signature for Payment Request Data"
243             return False
244
245         ### SIG Verified
246         self.details = pay_det = paymentrequest_pb2.PaymentDetails()
247         self.details.ParseFromString(paymntreq.serialized_payment_details)
248
249         for o in pay_det.outputs:
250             addr = transaction.get_address_from_output_script(o.script)[1]
251             self.outputs.append( (addr, o.amount) )
252
253         self.memo = self.details.memo
254
255         if CA_match:
256             self.status = 'Signed by Trusted CA:\n' + CA_OU
257         else:
258             self.status = "Supplied CA Not Found in Trusted CA Store."
259
260         self.payment_url = self.details.payment_url
261
262         if self.has_expired():
263             self.error = "ERROR: Payment Request has Expired."
264             return False
265
266         return True
267
268     def has_expired(self):
269         return self.details.expires and self.details.expires < int(time.time())
270
271     def get_amount(self):
272         return sum(map(lambda x:x[1], self.outputs))
273
274     def get_domain(self):
275         return self.domain
276
277     def get_memo(self):
278         return self.memo
279
280     def get_id(self):
281         return self.id
282
283     def get_outputs(self):
284         return self.outputs[:]
285
286     def send_ack(self, raw_tx, refund_addr):
287
288         pay_det = self.details
289         if not self.details.payment_url:
290             return False, "no url"
291
292         paymnt = paymentrequest_pb2.Payment()
293         paymnt.merchant_data = pay_det.merchant_data
294         paymnt.transactions.append(raw_tx)
295
296         ref_out = paymnt.refund_to.add()
297         ref_out.script = transaction.Transaction.pay_script(refund_addr)
298         paymnt.memo = "Paid using Electrum"
299         pm = paymnt.SerializeToString()
300
301         payurl = urlparse.urlparse(pay_det.payment_url)
302         try:
303             r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path)
304         except requests.exceptions.SSLError:
305             print "Payment Message/PaymentACK verify Failed"
306             try:
307                 r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False)
308             except Exception as e:
309                 print e
310                 return False, "Payment Message/PaymentACK Failed"
311
312         if r.status_code >= 500:
313             return False, r.reason
314
315         try:
316             paymntack = paymentrequest_pb2.PaymentACK()
317             paymntack.ParseFromString(r.content)
318         except Exception:
319             return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received."
320
321         print "PaymentACK message received: %s" % paymntack.memo
322         return True, paymntack.memo
323
324
325
326 if __name__ == "__main__":
327
328     try:
329         uri = sys.argv[1]
330     except:
331         print "usage: %s url"%sys.argv[0]
332         print "example url: \"bitcoin:mpu3yTLdqA1BgGtFUwkVJmhnU3q5afaFkf?r=https%3A%2F%2Fbitcoincore.org%2F%7Egavin%2Ff.php%3Fh%3D2a828c05b8b80dc440c80a5d58890298&amount=1\""
333         sys.exit(1)
334
335     address, amount, label, message, request_url, url = util.parse_url(uri)
336     pr = PaymentRequest(request_url)
337     if not pr.verify():
338         sys.exit(1)
339
340     print 'Payment Request Verified Domain: ', pr.domain
341     print 'outputs', pr.outputs
342     print 'Payment Memo: ', pr.details.memo
343
344     tx = "blah"
345     pr.send_ack(tx, refund_addr = "1vXAXUnGitimzinpXrqDWVU4tyAAQ34RA")
346