add support for RSA_SHA256
[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             sig = bytearray(sig[5:])
193             pubkey = x.publicKey
194             if algo.getComponentByName('algorithm') == x509.ALGO_RSA_SHA1:
195                 verify = pubkey.hashAndVerify(sig, data)
196             elif algo.getComponentByName('algorithm') == x509.ALGO_RSA_SHA256:
197                 hashBytes = bytearray(hashlib.sha256(data).digest())
198                 prefixBytes = bytearray([0x30,0x31,0x30,0x0d,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01,0x05,0x00,0x04,0x20])
199                 verify = pubkey.verify(sig, prefixBytes + hashBytes)
200             else:
201                 self.error = "Algorithm not supported" 
202                 util.print_error(self.error, algo.getComponentByName('algorithm'))
203                 return
204
205             if not verify:
206                 self.error = "Certificate not Signed by Provided CA Certificate Chain"
207                 return
208
209         ca = x509_chain[cert_num-1]
210         supplied_CA_fingerprint = ca.getFingerprint()
211         supplied_CA_names = ca.extract_names()
212         CA_OU = supplied_CA_names['OU']
213
214         x = ca_list.get(supplied_CA_fingerprint)
215         if x:
216             x.slow_parse()
217             names = x.extract_names()
218             CA_match = True
219             if names['CN'] != supplied_CA_names['CN']:
220                 print "ERROR: Trusted CA CN Mismatch; however CA has trusted fingerprint"
221                 print "Payment will continue with manual verification."
222         else:
223             CA_match = False
224
225         pubkey0 = x509_chain[0].publicKey
226         sig = paymntreq.signature
227         paymntreq.signature = ''
228         s = paymntreq.SerializeToString()
229         sigBytes = bytearray(sig)
230         msgBytes = bytearray(s)
231
232         if paymntreq.pki_type == "x509+sha256":
233             hashBytes = bytearray(hashlib.sha256(msgBytes).digest())
234             prefixBytes = bytearray([0x30,0x31,0x30,0x0d,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01,0x05,0x00,0x04,0x20])
235             verify = pubkey0.verify(sigBytes, prefixBytes + hashBytes)
236         elif paymntreq.pki_type == "x509+sha1":
237             verify = pubkey0.hashAndVerify(sigBytes, msgBytes)
238         else:
239             self.error = "ERROR: Unsupported PKI Type for Message Signature"
240             return False
241
242         if not verify:
243             self.error = "ERROR: Invalid Signature for Payment Request Data"
244             return False
245
246         ### SIG Verified
247         self.details = pay_det = paymentrequest_pb2.PaymentDetails()
248         self.details.ParseFromString(paymntreq.serialized_payment_details)
249
250         for o in pay_det.outputs:
251             addr = transaction.get_address_from_output_script(o.script)[1]
252             self.outputs.append( (addr, o.amount) )
253
254         self.memo = self.details.memo
255
256         if CA_match:
257             self.status = 'Signed by Trusted CA:\n' + CA_OU
258         else:
259             self.status = "Supplied CA Not Found in Trusted CA Store."
260
261         self.payment_url = self.details.payment_url
262
263         return True
264
265     def has_expired(self):
266         return self.details.expires and self.details.expires < int(time.time())
267
268     def get_expiration_date(self):
269         return self.details.expires
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:17KjQgnXC96jakzJe9yo8zxqerhqNptmhq?amount=0.0018&r=https%3A%2F%2Fbitpay.com%2Fi%2FMXc7qTM5f87EC62SWiS94z\""
333         sys.exit(1)
334
335     address, amount, label, message, request_url = util.parse_URI(uri)
336     from simple_config import SimpleConfig
337     config = SimpleConfig()
338     pr = PaymentRequest(config)
339     pr.read(request_url)
340     if not pr.verify():
341         print 'verify failed'
342         print pr.error
343         sys.exit(1)
344
345     print 'Payment Request Verified Domain: ', pr.domain
346     print 'outputs', pr.outputs
347     print 'Payment Memo: ', pr.details.memo
348
349     tx = "blah"
350     pr.send_ack(tx, refund_addr = "1vXAXUnGitimzinpXrqDWVU4tyAAQ34RA")
351