1
0

openalias: always enforce DNSSEC validation succeeds

This commit is contained in:
SomberNight
2025-12-05 16:21:38 +00:00
parent 49430e9722
commit cdcac8cb09
6 changed files with 20 additions and 30 deletions

View File

@@ -870,12 +870,10 @@ class Commands(Logger):
if x is None: if x is None:
return None return None
out = await wallet.contacts.resolve(x) 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'] return out['address']
@command('n') @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 Sweep private keys. Returns a transaction that spends UTXOs from
privkey to a destination address. The transaction will not be broadcast. 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:fee:Transaction fee (absolute, in BTC)
arg:decimal:feerate:Transaction fee rate (in sat/vbyte) arg:decimal:feerate:Transaction fee rate (in sat/vbyte)
arg:int:imax:Maximum number of inputs arg:int:imax:Maximum number of inputs
arg:bool:nocheck:Do not verify aliases
""" """
from .wallet import sweep from .wallet import sweep
fee_policy = self._get_fee_policy(fee, feerate) fee_policy = self._get_fee_policy(fee, feerate)
privkeys = privkey.split() privkeys = privkey.split()
self.nocheck = nocheck
#dest = self._resolver(destination) #dest = self._resolver(destination)
tx = await sweep( tx = await sweep(
privkeys, privkeys,
@@ -942,7 +938,7 @@ class Commands(Logger):
@command('wp') @command('wp')
async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, 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. """Create an on-chain transaction.
arg:str:destination:Bitcoin address, contact or alias 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:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet
arg:int:locktime:Set locktime block number arg:int:locktime:Set locktime block number
arg:bool:unsigned:Do not sign transaction 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) arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address)
""" """
return await self.paytomany( return await self.paytomany(
@@ -965,7 +960,6 @@ class Commands(Logger):
from_addr=from_addr, from_addr=from_addr,
from_coins=from_coins, from_coins=from_coins,
change_addr=change_addr, change_addr=change_addr,
nocheck=nocheck,
unsigned=unsigned, unsigned=unsigned,
rbf=rbf, rbf=rbf,
password=password, password=password,
@@ -976,7 +970,7 @@ class Commands(Logger):
@command('wp') @command('wp')
async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, 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. """Create a multi-output transaction.
arg:json:outputs:json list of ["address", "amount in BTC"] 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:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet
arg:int:locktime:Set locktime block number arg:int:locktime:Set locktime block number
arg:bool:unsigned:Do not sign transaction 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) 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) fee_policy = self._get_fee_policy(fee, feerate)
domain_addr = from_addr.split(',') if from_addr else None domain_addr = from_addr.split(',') if from_addr else None
domain_coins = from_coins.split(',') if from_coins 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 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') @command('w')
async def searchcontacts(self, query, wallet: Abstract_Wallet = None): async def searchcontacts(self, query, wallet: Abstract_Wallet = None):

View File

@@ -107,12 +107,11 @@ class Contacts(dict, Logger):
async def resolve_openalias(cls, url: str) -> Dict[str, Any]: async def resolve_openalias(cls, url: str) -> Dict[str, Any]:
out = await cls._resolve_openalias(url) out = await cls._resolve_openalias(url)
if out: if out:
address, name, validated = out address, name = out
return { return {
'address': address, 'address': address,
'name': name, 'name': name,
'type': 'openalias', 'type': 'openalias',
'validated': validated
} }
return {} return {}
@@ -138,7 +137,7 @@ class Contacts(dict, Logger):
asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop())
@classmethod @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 # support email-style addresses, per the OA standard
url = url.replace('@', '.') url = url.replace('@', '.')
try: try:
@@ -146,6 +145,9 @@ class Contacts(dict, Logger):
except DNSException as e: except DNSException as e:
_logger.info(f'Error resolving openalias: {repr(e)}') _logger.info(f'Error resolving openalias: {repr(e)}')
return None 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' prefix = 'btc'
for record in records: for record in records:
if record.rdtype != dns.rdatatype.TXT: if record.rdtype != dns.rdatatype.TXT:
@@ -158,7 +160,7 @@ class Contacts(dict, Logger):
name = address name = address
if not address: if not address:
continue continue
return address, name, validated return address, name
return None return None
@staticmethod @staticmethod

View File

@@ -137,7 +137,11 @@ async def _get_and_validate(ns, url, _type) -> dns.rrset.RRset:
return 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?) # 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 # 8.8.8.8 is Google's public DNS server
nameservers = ['8.8.8.8'] nameservers = ['8.8.8.8']

View File

@@ -437,8 +437,7 @@ class SettingsDialog(QDialog, QtEventListener):
self.alias_e.setStyleSheet("") self.alias_e.setStyleSheet("")
return return
if self.wallet.contacts.alias_info: if self.wallet.contacts.alias_info:
alias_addr, alias_name, validated = self.wallet.contacts.alias_info self.alias_e.setStyleSheet(ColorScheme.GREEN.as_stylesheet(True))
self.alias_e.setStyleSheet((ColorScheme.GREEN if validated else ColorScheme.RED).as_stylesheet(True))
else: else:
self.alias_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) self.alias_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))

View File

@@ -330,10 +330,6 @@ class PaymentIdentifier(Logger):
elif openalias_result := await openalias_task: elif openalias_result := await openalias_task:
self.openalias_data = openalias_result self.openalias_data = openalias_result
address = openalias_result.get('address') 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: try:
# this assertion error message is shown in the GUI # this assertion error message is shown in the GUI
assert bitcoin.is_address(address), f"{_('Openalias address invalid')}: {address[:100]}" assert bitcoin.is_address(address), f"{_('Openalias address invalid')}: {address[:100]}"
@@ -594,10 +590,6 @@ class PaymentIdentifier(Logger):
name = self.openalias_data.get('name') name = self.openalias_data.get('name')
description = name description = name
recipient = key + ' <' + address + '>' 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: elif self.bolt11:
recipient, amount, description = self._get_bolt11_fields() recipient, amount, description = self._get_bolt11_fields()

View File

@@ -225,9 +225,6 @@ class PaymentRequest:
sig = pr.signature sig = pr.signature
alias = pr.pki_data alias = pr.pki_data
info: dict = await Contacts.resolve_openalias(alias) 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": if pr.pki_type == "dnssec+btc":
self.requestor = alias self.requestor = alias
address = info.get('address') address = info.get('address')