URI: 
       tpaymentrequest.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tpaymentrequest.py (18103B)
       ---
            1 #!/usr/bin/env python
            2 #
            3 # Electrum - lightweight Bitcoin client
            4 # Copyright (C) 2014 Thomas Voegtlin
            5 #
            6 # Permission is hereby granted, free of charge, to any person
            7 # obtaining a copy of this software and associated documentation files
            8 # (the "Software"), to deal in the Software without restriction,
            9 # including without limitation the rights to use, copy, modify, merge,
           10 # publish, distribute, sublicense, and/or sell copies of the Software,
           11 # and to permit persons to whom the Software is furnished to do so,
           12 # subject to the following conditions:
           13 #
           14 # The above copyright notice and this permission notice shall be
           15 # included in all copies or substantial portions of the Software.
           16 #
           17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
           18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
           19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
           20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
           21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
           22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
           23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
           24 # SOFTWARE.
           25 import hashlib
           26 import sys
           27 import time
           28 from typing import Optional, List, TYPE_CHECKING
           29 import asyncio
           30 import urllib.parse
           31 
           32 import certifi
           33 import aiohttp
           34 
           35 
           36 try:
           37     from . import paymentrequest_pb2 as pb2
           38 except ImportError:
           39     # sudo apt-get install protobuf-compiler
           40     sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'protoc --proto_path=electrum/ --python_out=electrum/ electrum/paymentrequest.proto'")
           41 
           42 from . import bitcoin, constants, ecc, util, transaction, x509, rsakey
           43 from .util import bh2u, bfh, make_aiohttp_session
           44 from .invoices import OnchainInvoice
           45 from .crypto import sha256
           46 from .bitcoin import address_to_script
           47 from .transaction import PartialTxOutput
           48 from .network import Network
           49 from .logging import get_logger, Logger
           50 
           51 if TYPE_CHECKING:
           52     from .simple_config import SimpleConfig
           53 
           54 
           55 _logger = get_logger(__name__)
           56 
           57 
           58 REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
           59 ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
           60 
           61 ca_path = certifi.where()
           62 ca_list = None
           63 ca_keyID = None
           64 
           65 def load_ca_list():
           66     global ca_list, ca_keyID
           67     if ca_list is None:
           68         ca_list, ca_keyID = x509.load_certificates(ca_path)
           69 
           70 
           71 
           72 
           73 async def get_payment_request(url: str) -> 'PaymentRequest':
           74     u = urllib.parse.urlparse(url)
           75     error = None
           76     if u.scheme in ('http', 'https'):
           77         resp_content = None
           78         try:
           79             proxy = Network.get_instance().proxy
           80             async with make_aiohttp_session(proxy, headers=REQUEST_HEADERS) as session:
           81                 async with session.get(url) as response:
           82                     resp_content = await response.read()
           83                     response.raise_for_status()
           84                     # Guard against `bitcoin:`-URIs with invalid payment request URLs
           85                     if "Content-Type" not in response.headers \
           86                     or response.headers["Content-Type"] != "application/bitcoin-paymentrequest":
           87                         data = None
           88                         error = "payment URL not pointing to a payment request handling server"
           89                     else:
           90                         data = resp_content
           91                     data_len = len(data) if data is not None else None
           92                     _logger.info(f'fetched payment request {url} {data_len}')
           93         except (aiohttp.ClientError, asyncio.TimeoutError) as e:
           94             error = f"Error while contacting payment URL: {url}.\nerror type: {type(e)}"
           95             if isinstance(e, aiohttp.ClientResponseError):
           96                 error += f"\nGot HTTP status code {e.status}."
           97                 if resp_content:
           98                     try:
           99                         error_text_received = resp_content.decode("utf8")
          100                     except UnicodeDecodeError:
          101                         error_text_received = "(failed to decode error)"
          102                     else:
          103                         error_text_received = error_text_received[:400]
          104                     error_oneline = ' -- '.join(error.split('\n'))
          105                     _logger.info(f"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] "
          106                                  f"{repr(e)} text: {error_text_received}")
          107             data = None
          108     elif u.scheme == 'file':
          109         try:
          110             with open(u.path, 'r', encoding='utf-8') as f:
          111                 data = f.read()
          112         except IOError:
          113             data = None
          114             error = "payment URL not pointing to a valid file"
          115     else:
          116         data = None
          117         error = f"Unknown scheme for payment request. URL: {url}"
          118     pr = PaymentRequest(data, error=error)
          119     return pr
          120 
          121 
          122 class PaymentRequest:
          123 
          124     def __init__(self, data, *, error=None):
          125         self.raw = data
          126         self.error = error  # FIXME overloaded and also used when 'verify' succeeds
          127         self.parse(data)
          128         self.requestor = None # known after verify
          129         self.tx = None
          130 
          131     def __str__(self):
          132         return str(self.raw)
          133 
          134     def parse(self, r):
          135         self.outputs = []  # type: List[PartialTxOutput]
          136         if self.error:
          137             return
          138         self.id = bh2u(sha256(r)[0:16])
          139         try:
          140             self.data = pb2.PaymentRequest()
          141             self.data.ParseFromString(r)
          142         except:
          143             self.error = "cannot parse payment request"
          144             return
          145         self.details = pb2.PaymentDetails()
          146         self.details.ParseFromString(self.data.serialized_payment_details)
          147         pr_network = self.details.network
          148         client_network = 'test' if constants.net.TESTNET else 'main'
          149         if pr_network != client_network:
          150             self.error = (f'Payment request network "{pr_network}" does not'
          151                           f' match client network "{client_network}".')
          152             return
          153         for o in self.details.outputs:
          154             addr = transaction.get_address_from_output_script(o.script)
          155             if not addr:
          156                 # TODO maybe rm restriction but then get_requestor and get_id need changes
          157                 self.error = "only addresses are allowed as outputs"
          158                 return
          159             self.outputs.append(PartialTxOutput.from_address_and_value(addr, o.amount))
          160         self.memo = self.details.memo
          161         self.payment_url = self.details.payment_url
          162 
          163     def verify(self, contacts):
          164         if self.error:
          165             return False
          166         if not self.raw:
          167             self.error = "Empty request"
          168             return False
          169         pr = pb2.PaymentRequest()
          170         try:
          171             pr.ParseFromString(self.raw)
          172         except:
          173             self.error = "Error: Cannot parse payment request"
          174             return False
          175         if not pr.signature:
          176             # the address will be displayed as requestor
          177             self.requestor = None
          178             return True
          179         if pr.pki_type in ["x509+sha256", "x509+sha1"]:
          180             return self.verify_x509(pr)
          181         elif pr.pki_type in ["dnssec+btc", "dnssec+ecdsa"]:
          182             return self.verify_dnssec(pr, contacts)
          183         else:
          184             self.error = "ERROR: Unsupported PKI Type for Message Signature"
          185             return False
          186 
          187     def verify_x509(self, paymntreq):
          188         load_ca_list()
          189         if not ca_list:
          190             self.error = "Trusted certificate authorities list not found"
          191             return False
          192         cert = pb2.X509Certificates()
          193         cert.ParseFromString(paymntreq.pki_data)
          194         # verify the chain of certificates
          195         try:
          196             x, ca = verify_cert_chain(cert.certificate)
          197         except BaseException as e:
          198             _logger.exception('')
          199             self.error = str(e)
          200             return False
          201         # get requestor name
          202         self.requestor = x.get_common_name()
          203         if self.requestor.startswith('*.'):
          204             self.requestor = self.requestor[2:]
          205         # verify the BIP70 signature
          206         pubkey0 = rsakey.RSAKey(x.modulus, x.exponent)
          207         sig = paymntreq.signature
          208         paymntreq.signature = b''
          209         s = paymntreq.SerializeToString()
          210         sigBytes = bytearray(sig)
          211         msgBytes = bytearray(s)
          212         if paymntreq.pki_type == "x509+sha256":
          213             hashBytes = bytearray(hashlib.sha256(msgBytes).digest())
          214             verify = pubkey0.verify(sigBytes, x509.PREFIX_RSA_SHA256 + hashBytes)
          215         elif paymntreq.pki_type == "x509+sha1":
          216             verify = pubkey0.hashAndVerify(sigBytes, msgBytes)
          217         else:
          218             self.error = f"ERROR: unknown pki_type {paymntreq.pki_type} in Payment Request"
          219             return False
          220         if not verify:
          221             self.error = "ERROR: Invalid Signature for Payment Request Data"
          222             return False
          223         ### SIG Verified
          224         self.error = 'Signed by Trusted CA: ' + ca.get_common_name()
          225         return True
          226 
          227     def verify_dnssec(self, pr, contacts):
          228         sig = pr.signature
          229         alias = pr.pki_data
          230         info = contacts.resolve(alias)
          231         if info.get('validated') is not True:
          232             self.error = "Alias verification failed (DNSSEC)"
          233             return False
          234         if pr.pki_type == "dnssec+btc":
          235             self.requestor = alias
          236             address = info.get('address')
          237             pr.signature = b''
          238             message = pr.SerializeToString()
          239             if ecc.verify_message_with_address(address, sig, message):
          240                 self.error = 'Verified with DNSSEC'
          241                 return True
          242             else:
          243                 self.error = "verify failed"
          244                 return False
          245         else:
          246             self.error = "unknown algo"
          247             return False
          248 
          249     def has_expired(self) -> Optional[bool]:
          250         if not hasattr(self, 'details'):
          251             return None
          252         return self.details.expires and self.details.expires < int(time.time())
          253 
          254     def get_time(self):
          255         return self.details.time
          256 
          257     def get_expiration_date(self):
          258         return self.details.expires
          259 
          260     def get_amount(self):
          261         return sum(map(lambda x:x.value, self.outputs))
          262 
          263     def get_address(self):
          264         o = self.outputs[0]
          265         addr = o.address
          266         assert addr
          267         return addr
          268 
          269     def get_requestor(self):
          270         return self.requestor if self.requestor else self.get_address()
          271 
          272     def get_verify_status(self):
          273         return self.error if self.requestor else "No Signature"
          274 
          275     def get_memo(self):
          276         return self.memo
          277 
          278     def get_id(self):
          279         return self.id if self.requestor else self.get_address()
          280 
          281     def get_outputs(self):
          282         return self.outputs[:]
          283 
          284     async def send_payment_and_receive_paymentack(self, raw_tx, refund_addr):
          285         pay_det = self.details
          286         if not self.details.payment_url:
          287             return False, "no url"
          288         paymnt = pb2.Payment()
          289         paymnt.merchant_data = pay_det.merchant_data
          290         paymnt.transactions.append(bfh(raw_tx))
          291         ref_out = paymnt.refund_to.add()
          292         ref_out.script = util.bfh(address_to_script(refund_addr))
          293         paymnt.memo = "Paid using Electrum"
          294         pm = paymnt.SerializeToString()
          295         payurl = urllib.parse.urlparse(pay_det.payment_url)
          296         resp_content = None
          297         try:
          298             proxy = Network.get_instance().proxy
          299             async with make_aiohttp_session(proxy, headers=ACK_HEADERS) as session:
          300                 async with session.post(payurl.geturl(), data=pm) as response:
          301                     resp_content = await response.read()
          302                     response.raise_for_status()
          303                     try:
          304                         paymntack = pb2.PaymentACK()
          305                         paymntack.ParseFromString(resp_content)
          306                     except Exception:
          307                         return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received."
          308                     print(f"PaymentACK message received: {paymntack.memo}")
          309                     return True, paymntack.memo
          310         except aiohttp.ClientError as e:
          311             error = f"Payment Message/PaymentACK Failed:\nerror type: {type(e)}"
          312             if isinstance(e, aiohttp.ClientResponseError):
          313                 error += f"\nGot HTTP status code {e.status}."
          314                 if resp_content:
          315                     try:
          316                         error_text_received = resp_content.decode("utf8")
          317                     except UnicodeDecodeError:
          318                         error_text_received = "(failed to decode error)"
          319                     else:
          320                         error_text_received = error_text_received[:400]
          321                     error_oneline = ' -- '.join(error.split('\n'))
          322                     _logger.info(f"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] "
          323                                  f"{repr(e)} text: {error_text_received}")
          324             return False, error
          325 
          326 
          327 def make_unsigned_request(req: 'OnchainInvoice'):
          328     addr = req.get_address()
          329     time = req.time
          330     exp = req.exp
          331     if time and type(time) != int:
          332         time = 0
          333     if exp and type(exp) != int:
          334         exp = 0
          335     amount = req.amount_sat
          336     if amount is None:
          337         amount = 0
          338     memo = req.message
          339     script = bfh(address_to_script(addr))
          340     outputs = [(script, amount)]
          341     pd = pb2.PaymentDetails()
          342     if constants.net.TESTNET:
          343         pd.network = 'test'
          344     for script, amount in outputs:
          345         pd.outputs.add(amount=amount, script=script)
          346     pd.time = time
          347     pd.expires = time + exp if exp else 0
          348     pd.memo = memo
          349     pr = pb2.PaymentRequest()
          350     pr.serialized_payment_details = pd.SerializeToString()
          351     pr.signature = util.to_bytes('')
          352     return pr
          353 
          354 
          355 def sign_request_with_alias(pr, alias, alias_privkey):
          356     pr.pki_type = 'dnssec+btc'
          357     pr.pki_data = str(alias)
          358     message = pr.SerializeToString()
          359     ec_key = ecc.ECPrivkey(alias_privkey)
          360     compressed = bitcoin.is_compressed_privkey(alias_privkey)
          361     pr.signature = ec_key.sign_message(message, compressed)
          362 
          363 
          364 def verify_cert_chain(chain):
          365     """ Verify a chain of certificates. The last certificate is the CA"""
          366     load_ca_list()
          367     # parse the chain
          368     cert_num = len(chain)
          369     x509_chain = []
          370     for i in range(cert_num):
          371         x = x509.X509(bytearray(chain[i]))
          372         x509_chain.append(x)
          373         if i == 0:
          374             x.check_date()
          375         else:
          376             if not x.check_ca():
          377                 raise Exception("ERROR: Supplied CA Certificate Error")
          378     if not cert_num > 1:
          379         raise Exception("ERROR: CA Certificate Chain Not Provided by Payment Processor")
          380     # if the root CA is not supplied, add it to the chain
          381     ca = x509_chain[cert_num-1]
          382     if ca.getFingerprint() not in ca_list:
          383         keyID = ca.get_issuer_keyID()
          384         f = ca_keyID.get(keyID)
          385         if f:
          386             root = ca_list[f]
          387             x509_chain.append(root)
          388         else:
          389             raise Exception("Supplied CA Not Found in Trusted CA Store.")
          390     # verify the chain of signatures
          391     cert_num = len(x509_chain)
          392     for i in range(1, cert_num):
          393         x = x509_chain[i]
          394         prev_x = x509_chain[i-1]
          395         algo, sig, data = prev_x.get_signature()
          396         sig = bytearray(sig)
          397         pubkey = rsakey.RSAKey(x.modulus, x.exponent)
          398         if algo == x509.ALGO_RSA_SHA1:
          399             verify = pubkey.hashAndVerify(sig, data)
          400         elif algo == x509.ALGO_RSA_SHA256:
          401             hashBytes = bytearray(hashlib.sha256(data).digest())
          402             verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA256 + hashBytes)
          403         elif algo == x509.ALGO_RSA_SHA384:
          404             hashBytes = bytearray(hashlib.sha384(data).digest())
          405             verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA384 + hashBytes)
          406         elif algo == x509.ALGO_RSA_SHA512:
          407             hashBytes = bytearray(hashlib.sha512(data).digest())
          408             verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA512 + hashBytes)
          409         else:
          410             raise Exception("Algorithm not supported: {}".format(algo))
          411         if not verify:
          412             raise Exception("Certificate not Signed by Provided CA Certificate Chain")
          413 
          414     return x509_chain[0], ca
          415 
          416 
          417 def check_ssl_config(config):
          418     from . import pem
          419     key_path = config.get('ssl_keyfile')
          420     cert_path = config.get('ssl_certfile')
          421     with open(key_path, 'r', encoding='utf-8') as f:
          422         params = pem.parse_private_key(f.read())
          423     with open(cert_path, 'r', encoding='utf-8') as f:
          424         s = f.read()
          425     bList = pem.dePemList(s, "CERTIFICATE")
          426     # verify chain
          427     x, ca = verify_cert_chain(bList)
          428     # verify that privkey and pubkey match
          429     privkey = rsakey.RSAKey(*params)
          430     pubkey = rsakey.RSAKey(x.modulus, x.exponent)
          431     assert x.modulus == params[0]
          432     assert x.exponent == params[1]
          433     # return requestor
          434     requestor = x.get_common_name()
          435     if requestor.startswith('*.'):
          436         requestor = requestor[2:]
          437     return requestor
          438 
          439 def sign_request_with_x509(pr, key_path, cert_path):
          440     from . import pem
          441     with open(key_path, 'r', encoding='utf-8') as f:
          442         params = pem.parse_private_key(f.read())
          443         privkey = rsakey.RSAKey(*params)
          444     with open(cert_path, 'r', encoding='utf-8') as f:
          445         s = f.read()
          446         bList = pem.dePemList(s, "CERTIFICATE")
          447     certificates = pb2.X509Certificates()
          448     certificates.certificate.extend(map(bytes, bList))
          449     pr.pki_type = 'x509+sha256'
          450     pr.pki_data = certificates.SerializeToString()
          451     msgBytes = bytearray(pr.SerializeToString())
          452     hashBytes = bytearray(hashlib.sha256(msgBytes).digest())
          453     sig = privkey.sign(x509.PREFIX_RSA_SHA256 + hashBytes)
          454     pr.signature = bytes(sig)
          455 
          456 
          457 def serialize_request(req):  # FIXME this is broken
          458     pr = make_unsigned_request(req)
          459     signature = req.get('sig')
          460     requestor = req.get('name')
          461     if requestor and signature:
          462         pr.signature = bfh(signature)
          463         pr.pki_type = 'dnssec+btc'
          464         pr.pki_data = str(requestor)
          465     return pr
          466 
          467 
          468 def make_request(config: 'SimpleConfig', req: 'OnchainInvoice'):
          469     pr = make_unsigned_request(req)
          470     key_path = config.get('ssl_keyfile')
          471     cert_path = config.get('ssl_certfile')
          472     if key_path and cert_path:
          473         sign_request_with_x509(pr, key_path, cert_path)
          474     return pr