store payment requests even if M2Crypto is not available
[electrum-nvc.git] / lib / paymentrequest.py
1 import hashlib
2 import httplib
3 import os.path
4 import re
5 import sys
6 import threading
7 import time
8 import traceback
9 import urllib2
10
11 try:
12     import paymentrequest_pb2
13 except:
14     print "protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto"
15     raise Exception()
16
17 try:
18     import requests
19 except ImportError:
20     sys.exit("Error: requests does not seem to be installed. Try 'sudo pip install requests'")
21
22 import urlparse
23
24
25 import bitcoin
26 import util
27 import transaction
28
29
30 REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
31 ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
32
33 # status can be:
34 PR_UNPAID  = 0
35 PR_EXPIRED = 1
36 PR_SENT    = 2     # sent but not propagated
37 PR_PAID    = 3     # send and propagated
38
39
40
41
42 ca_list = {}
43
44 def load_certificates():
45     try:
46         from M2Crypto import X509
47     except:
48         print_error("ERROR: Could not import M2Crypto")
49         return False
50
51     ca_path = os.path.expanduser("~/.electrum/ca/ca-bundle.crt")
52     try:
53         ca_f = open(ca_path, 'r')
54     except Exception:
55         print "ERROR: Could not open %s"%ca_path
56         print "ca-bundle.crt file should be placed in ~/.electrum/ca/ca-bundle.crt"
57         print "Documentation on how to download or create the file here: http://curl.haxx.se/docs/caextract.html"
58         print "Payment will continue with manual verification."
59         return False
60     c = ""
61     for line in ca_f:
62         if line == "-----BEGIN CERTIFICATE-----\n":
63             c = line
64         else:
65             c += line
66         if line == "-----END CERTIFICATE-----\n":
67             x = X509.load_cert_string(c)
68             ca_list[x.get_fingerprint()] = x
69     ca_f.close()
70     return True
71
72 load_certificates()
73
74
75 class PaymentRequest:
76     def __init__(self, config):
77         self.config = config
78         self.outputs = []
79         self.error = ""
80
81     def read(self, url):
82         self.url = url
83
84         u = urlparse.urlparse(url)
85         self.domain = u.netloc
86         try:
87             connection = httplib.HTTPConnection(u.netloc) if u.scheme == 'http' else httplib.HTTPSConnection(u.netloc)
88             connection.request("GET",u.geturl(), headers=REQUEST_HEADERS)
89             response = connection.getresponse()
90         except:
91             self.error = "cannot read url"
92             return
93
94         try:
95             r = response.read()
96         except:
97             self.error = "cannot read"
98             return
99
100         try:
101             self.data = paymentrequest_pb2.PaymentRequest()
102             self.data.ParseFromString(r)
103         except:
104             self.error = "cannot parse payment request"
105             return
106
107         self.id = bitcoin.sha256(r)[0:16].encode('hex')
108         print self.id
109
110         dir_path = os.path.join( self.config.path, 'requests')
111         if not os.path.exists(dir_path):
112             os.mkdir(dir_path)
113         filename = os.path.join(dir_path, self.id)
114         with open(filename,'w') as f:
115             f.write(r)
116
117
118
119     def verify(self):
120         try:
121             from M2Crypto import X509
122         except:
123             self.error = "cannot import M2Crypto"
124             return False
125
126         if not ca_list:
127             self.error = "Trusted certificate authorities list not found"
128             return False
129
130         paymntreq = self.data
131         sig = paymntreq.signature
132         if not sig:
133             self.error = "No signature"
134             return 
135
136         cert = paymentrequest_pb2.X509Certificates()
137         cert.ParseFromString(paymntreq.pki_data)
138         cert_num = len(cert.certificate)
139
140         x509_1 = X509.load_cert_der_string(cert.certificate[0])
141         if self.domain != x509_1.get_subject().CN:
142             validcert = False
143             try:
144                 SANs = x509_1.get_ext("subjectAltName").get_value().split(",")
145                 for s in SANs:
146                     s = s.strip()
147                     if s.startswith("DNS:") and s[4:] == self.domain:
148                         validcert = True
149                         print "Match SAN DNS"
150                     elif s.startswith("IP:") and s[3:] == self.domain:
151                         validcert = True
152                         print "Match SAN IP"
153                     elif s.startswith("email:") and s[6:] == self.domain:
154                         validcert = True
155                         print "Match SAN email"
156             except Exception, e:
157                 print "ERROR: No SAN data"
158             if not validcert:
159                 ###TODO: check for wildcards
160                 self.error = "ERROR: Certificate Subject Domain Mismatch and SAN Mismatch"
161                 return
162
163         x509 = []
164         CA_OU = ''
165
166         if cert_num > 1:
167             for i in range(cert_num - 1):
168                 x509.append(X509.load_cert_der_string(cert.certificate[i+1]))
169                 if x509[i].check_ca() == 0:
170                     self.error = "ERROR: Supplied CA Certificate Error"
171                     return
172             for i in range(cert_num - 1):
173                 if i == 0:
174                     if x509_1.verify(x509[i].get_pubkey()) != 1:
175                         self.error = "ERROR: Certificate not Signed by Provided CA Certificate Chain"
176                         return
177                 else:
178                     if x509[i-1].verify(x509[i].get_pubkey()) != 1:
179                         self.error = "ERROR: CA Certificate not Signed by Provided CA Certificate Chain"
180                         return
181
182             supplied_CA_fingerprint = x509[cert_num-2].get_fingerprint()
183             supplied_CA_CN = x509[cert_num-2].get_subject().CN
184             CA_match = False
185
186             x = ca_list.get(supplied_CA_fingerprint)
187             if x:
188                 CA_OU = x.get_subject().OU
189                 CA_match = True
190                 if x.get_subject().CN != supplied_CA_CN:
191                     print "ERROR: Trusted CA CN Mismatch; however CA has trusted fingerprint"
192                     print "Payment will continue with manual verification."
193             else:
194                 print "ERROR: Supplied CA Not Found in Trusted CA Store."
195                 print "Payment will continue with manual verification."
196         else:
197             self.error = "ERROR: CA Certificate Chain Not Provided by Payment Processor"
198             return False
199
200         paymntreq.signature = ''
201         s = paymntreq.SerializeToString()
202         pubkey_1 = x509_1.get_pubkey()
203
204         if paymntreq.pki_type == "x509+sha256":
205             pubkey_1.reset_context(md="sha256")
206         elif paymntreq.pki_type == "x509+sha1":
207             pubkey_1.reset_context(md="sha1")
208         else:
209             self.error = "ERROR: Unsupported PKI Type for Message Signature"
210             return False
211
212         pubkey_1.verify_init()
213         pubkey_1.verify_update(s)
214         if pubkey_1.verify_final(sig) != 1:
215             self.error = "ERROR: Invalid Signature for Payment Request Data"
216             return False
217
218         ### SIG Verified
219
220         self.payment_details = pay_det = paymentrequest_pb2.PaymentDetails()
221         pay_det.ParseFromString(paymntreq.serialized_payment_details)
222
223         if pay_det.expires and pay_det.expires < int(time.time()):
224             self.error = "ERROR: Payment Request has Expired."
225             return False
226
227         for o in pay_det.outputs:
228             addr = transaction.get_address_from_output_script(o.script)[1]
229             self.outputs.append( (addr, o.amount) )
230
231         self.memo = pay_det.memo
232
233         if CA_match:
234             self.status = 'Signed by Trusted CA:\n' + CA_OU
235
236         print "payment url", pay_det.payment_url
237         return True
238
239
240     def get_amount(self):
241         return sum(map(lambda x:x[1], self.outputs))
242
243     def get_domain(self):
244         return self.domain
245
246     def get_id(self):
247         return self.id
248
249     def send_ack(self, raw_tx, refund_addr):
250
251         pay_det = self.payment_details
252         if not pay_det.payment_url:
253             return False, "no url"
254
255         paymnt = paymentrequest_pb2.Payment()
256         paymnt.merchant_data = pay_det.merchant_data
257         paymnt.transactions.append(raw_tx)
258
259         ref_out = paymnt.refund_to.add()
260         ref_out.script = transaction.Transaction.pay_script(refund_addr)
261         paymnt.memo = "Paid using Electrum"
262         pm = paymnt.SerializeToString()
263
264         payurl = urlparse.urlparse(pay_det.payment_url)
265         try:
266             r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path)
267         except requests.exceptions.SSLError:
268             print "Payment Message/PaymentACK verify Failed"
269             try:
270                 r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False)
271             except Exception as e:
272                 print e
273                 return False, "Payment Message/PaymentACK Failed"
274
275         if r.status_code >= 500:
276             return False, r.reason
277
278         try:
279             paymntack = paymentrequest_pb2.PaymentACK()
280             paymntack.ParseFromString(r.content)
281         except Exception:
282             return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received."
283
284         print "PaymentACK message received: %s" % paymntack.memo
285         return True, paymntack.memo
286
287
288
289 if __name__ == "__main__":
290
291     try:
292         uri = sys.argv[1]
293     except:
294         print "usage: %s url"%sys.argv[0]
295         print "example url: \"bitcoin:mpu3yTLdqA1BgGtFUwkVJmhnU3q5afaFkf?r=https%3A%2F%2Fbitcoincore.org%2F%7Egavin%2Ff.php%3Fh%3D2a828c05b8b80dc440c80a5d58890298&amount=1\""
296         sys.exit(1)
297
298     address, amount, label, message, request_url, url = util.parse_url(uri)
299     pr = PaymentRequest(request_url)
300     if not pr.verify():
301         sys.exit(1)
302
303     print 'Payment Request Verified Domain: ', pr.domain
304     print 'outputs', pr.outputs
305     print 'Payment Memo: ', pr.payment_details.memo
306
307     tx = "blah"
308     pr.send_ack(tx, refund_addr = "1vXAXUnGitimzinpXrqDWVU4tyAAQ34RA")
309