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