diff --git a/electrum/commands.py b/electrum/commands.py index 6aef50cec..4bf97387b 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -870,12 +870,10 @@ class Commands(Logger): if x is None: return None out = await wallet.contacts.resolve(x) - if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False: - raise UserFacingException(f"cannot verify alias: {x}") return out['address'] @command('n') - async def sweep(self, privkey, destination, fee=None, feerate=None, nocheck=False, imax=100): + async def sweep(self, privkey, destination, fee=None, feerate=None, imax=100): """ Sweep private keys. Returns a transaction that spends UTXOs from privkey to a destination address. The transaction will not be broadcast. @@ -885,12 +883,10 @@ class Commands(Logger): arg:decimal:fee:Transaction fee (absolute, in BTC) arg:decimal:feerate:Transaction fee rate (in sat/vbyte) arg:int:imax:Maximum number of inputs - arg:bool:nocheck:Do not verify aliases """ from .wallet import sweep fee_policy = self._get_fee_policy(fee, feerate) privkeys = privkey.split() - self.nocheck = nocheck #dest = self._resolver(destination) tx = await sweep( privkeys, @@ -942,7 +938,7 @@ class Commands(Logger): @command('wp') async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, - nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None): + unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None): """Create an on-chain transaction. arg:str:destination:Bitcoin address, contact or alias @@ -955,7 +951,6 @@ class Commands(Logger): arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet arg:int:locktime:Set locktime block number arg:bool:unsigned:Do not sign transaction - arg:bool:nocheck:Do not verify aliases arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address) """ return await self.paytomany( @@ -965,7 +960,6 @@ class Commands(Logger): from_addr=from_addr, from_coins=from_coins, change_addr=change_addr, - nocheck=nocheck, unsigned=unsigned, rbf=rbf, password=password, @@ -976,7 +970,7 @@ class Commands(Logger): @command('wp') async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, - nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None): + unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None): """Create a multi-output transaction. arg:json:outputs:json list of ["address", "amount in BTC"] @@ -988,10 +982,8 @@ class Commands(Logger): arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet arg:int:locktime:Set locktime block number arg:bool:unsigned:Do not sign transaction - arg:bool:nocheck:Do not verify aliases arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address) """ - self.nocheck = nocheck fee_policy = self._get_fee_policy(fee, feerate) domain_addr = from_addr.split(',') if from_addr else None domain_coins = from_coins.split(',') if from_coins else None @@ -1150,7 +1142,11 @@ class Commands(Logger): arg:str:key:the alias to be retrieved """ - return await wallet.contacts.resolve(key) + d = await wallet.contacts.resolve(key) + if d.get("type") == "openalias": + # we always validate DNSSEC now + d["validated"] = True + return d @command('w') async def searchcontacts(self, query, wallet: Abstract_Wallet = None): diff --git a/electrum/contacts.py b/electrum/contacts.py index 76a5d1eb0..d3aa4da1f 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -107,12 +107,11 @@ class Contacts(dict, Logger): async def resolve_openalias(cls, url: str) -> Dict[str, Any]: out = await cls._resolve_openalias(url) if out: - address, name, validated = out + address, name = out return { 'address': address, 'name': name, 'type': 'openalias', - 'validated': validated } return {} @@ -138,7 +137,7 @@ class Contacts(dict, Logger): asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) @classmethod - async def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]: + async def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str]]: # support email-style addresses, per the OA standard url = url.replace('@', '.') try: @@ -146,6 +145,9 @@ class Contacts(dict, Logger): except DNSException as e: _logger.info(f'Error resolving openalias: {repr(e)}') return None + if not validated: # enforce DNSSEC validation. without it, DNS is completely insecure + _logger.info(f"DNSSEC validation failed for {url=!r}, or maybe dependencies are missing and could not even try.") + return None prefix = 'btc' for record in records: if record.rdtype != dns.rdatatype.TXT: @@ -158,7 +160,7 @@ class Contacts(dict, Logger): name = address if not address: continue - return address, name, validated + return address, name return None @staticmethod diff --git a/electrum/dnssec.py b/electrum/dnssec.py index 4b0e48d1b..c45c5050d 100644 --- a/electrum/dnssec.py +++ b/electrum/dnssec.py @@ -137,7 +137,11 @@ async def _get_and_validate(ns, url, _type) -> dns.rrset.RRset: return rrset -async def query(url, rtype) -> Tuple[dns.rrset.RRset, bool]: +async def query(url: str, rtype: dns.rdatatype.RdataType) -> Tuple[dns.rrset.RRset, bool]: + """Try to do DNS resolution, including DNSSEC. + 'validated' shows whether the DNSSEC checks passed. DNS is completely INSECURE without DNSSEC, + so the caller must carefully consider whether the response can be used for anything if validated=False. + """ # FIXME this method is not using the network proxy. (although the proxy might not support UDP?) # 8.8.8.8 is Google's public DNS server nameservers = ['8.8.8.8'] diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index bfb9c01c8..53010d201 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -437,8 +437,7 @@ class SettingsDialog(QDialog, QtEventListener): self.alias_e.setStyleSheet("") return if self.wallet.contacts.alias_info: - alias_addr, alias_name, validated = self.wallet.contacts.alias_info - self.alias_e.setStyleSheet((ColorScheme.GREEN if validated else ColorScheme.RED).as_stylesheet(True)) + self.alias_e.setStyleSheet(ColorScheme.GREEN.as_stylesheet(True)) else: self.alias_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index a71b1bfec..725dc44f7 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -330,10 +330,6 @@ class PaymentIdentifier(Logger): elif openalias_result := await openalias_task: self.openalias_data = openalias_result address = openalias_result.get('address') - if not openalias_result.get('validated'): - self.warning = _( - 'WARNING: the alias "{}" could not be validated via an additional ' - 'security check, DNSSEC, and thus may not be correct.').format(openalias_key) try: # this assertion error message is shown in the GUI assert bitcoin.is_address(address), f"{_('Openalias address invalid')}: {address[:100]}" @@ -594,10 +590,6 @@ class PaymentIdentifier(Logger): name = self.openalias_data.get('name') description = name recipient = key + ' <' + address + '>' - validated = self.openalias_data.get('validated') - if not validated: - self.warning = _('WARNING: the alias "{}" could not be validated via an additional ' - 'security check, DNSSEC, and thus may not be correct.').format(key) elif self.bolt11: recipient, amount, description = self._get_bolt11_fields() diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 15c7b4b68..dcd9c79a7 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -225,9 +225,6 @@ class PaymentRequest: sig = pr.signature alias = pr.pki_data info: dict = await Contacts.resolve_openalias(alias) - if info.get('validated') is not True: - self.error = "Alias verification failed (DNSSEC)" - return False if pr.pki_type == "dnssec+btc": self.requestor = alias address = info.get('address')