You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

310 lines
9.7 KiB

import hashlib
import httplib
import os.path
import re
import sys
import threading
import time
import traceback
import urllib2
try:
import paymentrequest_pb2
except:
print "protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto"
raise Exception()
try:
import requests
except ImportError:
sys.exit("Error: requests does not seem to be installed. Try 'sudo pip install requests'")
import urlparse
import bitcoin
import util
import transaction
REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
11 years ago
# status can be:
PR_UNPAID = 0
PR_EXPIRED = 1
PR_SENT = 2 # sent but not propagated
PR_PAID = 3 # send and propagated
ca_list = {}
def load_certificates():
try:
from M2Crypto import X509
except:
print_error("ERROR: Could not import M2Crypto")
return False
ca_path = os.path.expanduser("~/.electrum/ca/ca-bundle.crt")
try:
ca_f = open(ca_path, 'r')
except Exception:
print "ERROR: Could not open %s"%ca_path
print "ca-bundle.crt file should be placed in ~/.electrum/ca/ca-bundle.crt"
print "Documentation on how to download or create the file here: http://curl.haxx.se/docs/caextract.html"
print "Payment will continue with manual verification."
return False
c = ""
for line in ca_f:
if line == "-----BEGIN CERTIFICATE-----\n":
c = line
else:
c += line
if line == "-----END CERTIFICATE-----\n":
x = X509.load_cert_string(c)
ca_list[x.get_fingerprint()] = x
ca_f.close()
return True
load_certificates()
class PaymentRequest:
11 years ago
def __init__(self, config):
self.config = config
self.outputs = []
self.error = ""
11 years ago
def read(self, url):
self.url = url
11 years ago
u = urlparse.urlparse(url)
self.domain = u.netloc
try:
connection = httplib.HTTPConnection(u.netloc) if u.scheme == 'http' else httplib.HTTPSConnection(u.netloc)
connection.request("GET",u.geturl(), headers=REQUEST_HEADERS)
11 years ago
response = connection.getresponse()
except:
self.error = "cannot read url"
return
try:
11 years ago
r = response.read()
except:
self.error = "cannot read"
return
try:
self.data = paymentrequest_pb2.PaymentRequest()
self.data.ParseFromString(r)
except:
self.error = "cannot parse payment request"
return
11 years ago
self.id = bitcoin.sha256(r)[0:16].encode('hex')
print self.id
dir_path = os.path.join( self.config.path, 'requests')
if not os.path.exists(dir_path):
os.mkdir(dir_path)
filename = os.path.join(dir_path, self.id)
with open(filename,'w') as f:
f.write(r)
def verify(self):
try:
from M2Crypto import X509
except:
self.error = "cannot import M2Crypto"
return False
if not ca_list:
self.error = "Trusted certificate authorities list not found"
return False
11 years ago
paymntreq = self.data
sig = paymntreq.signature
if not sig:
self.error = "No signature"
return
cert = paymentrequest_pb2.X509Certificates()
cert.ParseFromString(paymntreq.pki_data)
cert_num = len(cert.certificate)
x509_1 = X509.load_cert_der_string(cert.certificate[0])
if self.domain != x509_1.get_subject().CN:
validcert = False
try:
SANs = x509_1.get_ext("subjectAltName").get_value().split(",")
for s in SANs:
s = s.strip()
if s.startswith("DNS:") and s[4:] == self.domain:
validcert = True
print "Match SAN DNS"
elif s.startswith("IP:") and s[3:] == self.domain:
validcert = True
print "Match SAN IP"
elif s.startswith("email:") and s[6:] == self.domain:
validcert = True
print "Match SAN email"
except Exception, e:
print "ERROR: No SAN data"
if not validcert:
###TODO: check for wildcards
self.error = "ERROR: Certificate Subject Domain Mismatch and SAN Mismatch"
return
x509 = []
CA_OU = ''
if cert_num > 1:
for i in range(cert_num - 1):
x509.append(X509.load_cert_der_string(cert.certificate[i+1]))
if x509[i].check_ca() == 0:
self.error = "ERROR: Supplied CA Certificate Error"
return
for i in range(cert_num - 1):
if i == 0:
if x509_1.verify(x509[i].get_pubkey()) != 1:
self.error = "ERROR: Certificate not Signed by Provided CA Certificate Chain"
return
else:
if x509[i-1].verify(x509[i].get_pubkey()) != 1:
self.error = "ERROR: CA Certificate not Signed by Provided CA Certificate Chain"
return
supplied_CA_fingerprint = x509[cert_num-2].get_fingerprint()
supplied_CA_CN = x509[cert_num-2].get_subject().CN
CA_match = False
x = ca_list.get(supplied_CA_fingerprint)
if x:
CA_OU = x.get_subject().OU
CA_match = True
if x.get_subject().CN != supplied_CA_CN:
print "ERROR: Trusted CA CN Mismatch; however CA has trusted fingerprint"
print "Payment will continue with manual verification."
else:
print "ERROR: Supplied CA Not Found in Trusted CA Store."
print "Payment will continue with manual verification."
else:
self.error = "ERROR: CA Certificate Chain Not Provided by Payment Processor"
return False
paymntreq.signature = ''
s = paymntreq.SerializeToString()
pubkey_1 = x509_1.get_pubkey()
if paymntreq.pki_type == "x509+sha256":
pubkey_1.reset_context(md="sha256")
elif paymntreq.pki_type == "x509+sha1":
pubkey_1.reset_context(md="sha1")
else:
self.error = "ERROR: Unsupported PKI Type for Message Signature"
return False
pubkey_1.verify_init()
pubkey_1.verify_update(s)
if pubkey_1.verify_final(sig) != 1:
self.error = "ERROR: Invalid Signature for Payment Request Data"
return False
### SIG Verified
self.payment_details = pay_det = paymentrequest_pb2.PaymentDetails()
pay_det.ParseFromString(paymntreq.serialized_payment_details)
if pay_det.expires and pay_det.expires < int(time.time()):
self.error = "ERROR: Payment Request has Expired."
return False
for o in pay_det.outputs:
addr = transaction.get_address_from_output_script(o.script)[1]
self.outputs.append( (addr, o.amount) )
11 years ago
self.memo = pay_det.memo
if CA_match:
self.status = 'Signed by Trusted CA:\n' + CA_OU
11 years ago
print "payment url", pay_det.payment_url
return True
11 years ago
def get_amount(self):
return sum(map(lambda x:x[1], self.outputs))
def get_domain(self):
return self.domain
def get_id(self):
return self.id
def send_ack(self, raw_tx, refund_addr):
pay_det = self.payment_details
11 years ago
if not pay_det.payment_url:
return False, "no url"
11 years ago
paymnt = paymentrequest_pb2.Payment()
paymnt.merchant_data = pay_det.merchant_data
paymnt.transactions.append(raw_tx)
11 years ago
ref_out = paymnt.refund_to.add()
ref_out.script = transaction.Transaction.pay_script(refund_addr)
paymnt.memo = "Paid using Electrum"
pm = paymnt.SerializeToString()
11 years ago
payurl = urlparse.urlparse(pay_det.payment_url)
try:
r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path)
except requests.exceptions.SSLError:
print "Payment Message/PaymentACK verify Failed"
try:
11 years ago
r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False)
except Exception as e:
print e
return False, "Payment Message/PaymentACK Failed"
if r.status_code >= 500:
return False, r.reason
11 years ago
try:
paymntack = paymentrequest_pb2.PaymentACK()
paymntack.ParseFromString(r.content)
except Exception:
return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received."
11 years ago
print "PaymentACK message received: %s" % paymntack.memo
return True, paymntack.memo
if __name__ == "__main__":
try:
uri = sys.argv[1]
except:
print "usage: %s url"%sys.argv[0]
print "example url: \"bitcoin:mpu3yTLdqA1BgGtFUwkVJmhnU3q5afaFkf?r=https%3A%2F%2Fbitcoincore.org%2F%7Egavin%2Ff.php%3Fh%3D2a828c05b8b80dc440c80a5d58890298&amount=1\""
sys.exit(1)
address, amount, label, message, request_url, url = util.parse_url(uri)
pr = PaymentRequest(request_url)
if not pr.verify():
sys.exit(1)
print 'Payment Request Verified Domain: ', pr.domain
print 'outputs', pr.outputs
print 'Payment Memo: ', pr.payment_details.memo
tx = "blah"
pr.send_ack(tx, refund_addr = "1vXAXUnGitimzinpXrqDWVU4tyAAQ34RA")