openalias: always enforce DNSSEC validation succeeds
What changed, and why it matters
This commit removes the ability to skip DNSSEC security checks for OpenAlias lookups in the Electrum Bitcoin wallet. OpenAlias turns human-readable names like 'donate@example.com' into Bitcoin addresses using DNS. Previously, users or commands could pass a 'nocheck' flag to accept unvalidated DNS results, which is dangerous because DNS responses can be spoofed or tampered with. The patch now enforces that DNSSEC validation must succeed before any OpenAlias result is used, and it strips out the old 'nocheck' options and unvalidated warnings. This is a hardening change that closes a known weak path where an attacker on the network could trick the wallet into sending money to the wrong address.
Users and integrators should upgrade to a version containing this commit to ensure OpenAlias lookups cannot be used without successful DNSSEC validation. Anyone relying on the `nocheck` flag for sweep/payto/paytomany should stop using aliases in untrusted network environments or ensure proper DNSSEC deployment. No immediate incident response is indicated by the diff alone, but the change is a clear security hardening measure.
Security signals we found
Removal of a security bypass flag (`nocheck`) for alias verification
Fail-closed enforcement of DNSSEC validation in OpenAlias resolution
Removal of user warnings that previously allowed proceeding with unvalidated DNSSEC results
Docstring added stating DNS is completely insecure without DNSSEC
Change from optional validation to mandatory validation across CLI and GUI code paths
Evidence from the diff
The patch removes the nocheck parameter from CLI commands (sweep, payto, paytomany) and from internal resolution logic, and changes OpenAlias resolution to fail closed when DNSSEC validation does not succeed. In contacts.py, _resolve_openalias now returns None if validated is False, logging a failure. The validated field is removed from the returned tuple/dict. commands.py drops nocheck arguments and, in resolvealias, force-sets validated=True for OpenAlias results (since unvalidated results can no longer reach that point). payment_identifier.py and paymentrequest.py remove warnings and errors that previously allowed proceeding with unvalidated aliases. dnssec.py gets a docstring emphasizing that DNS is insecure without DNSSEC. settings_dialog.py no longer shows a red style for unvalidated aliases.
Changed components
electrum/commands.pyelectrum/contacts.pyelectrum/dnssec.pyelectrum/gui/qt/settings_dialog.pyelectrum/payment_identifier.pyelectrum/paymentrequest.pyInspect captured patch +20 / −30
diff --git a/electrum/commands.py b/electrum/commands.py
index 6aef50c..4bf9738 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 76a5d1e..d3aa4da 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 4b0e48d..c45c505 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 bfb9c01..53010d2 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 a71b1bf..725dc44 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 15c7b4b..dcd9c79 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')
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.