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