openalias: always enforce DNSSEC validation succeeds
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user