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:
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):

View File

@@ -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

View File

@@ -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']

View File

@@ -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))

View File

@@ -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()

View File

@@ -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')