Attached is the debdiff.
There is a tracking bug filed against Electrum:
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1052200
--
Soren Stoutner
soren@stoutner.com
diff -Nru electrum-4.3.4+dfsg1/debian/changelog electrum-4.3.4+dfsg1/debian/changelog --- electrum-4.3.4+dfsg1/debian/changelog 2023-02-02 15:52:07.000000000 -0700 +++ electrum-4.3.4+dfsg1/debian/changelog 2023-09-12 09:51:54.000000000 -0700 @@ -1,3 +1,13 @@ +electrum (4.3.4+dfsg1-1+deb12u1) bookworm; urgency=high + + * Add debian/patches to fix Lightning security problem fixed upstream in + 4.4.6. See + https://github.com/spesmilo/electrum/security/advisories/GHSA-8r85-vp7r-hjxf + * Add myself to uploaders and remove Tristan Seligmann <mithrandi@debian.org>. + (Closes: #1041171). + + -- Soren Stoutner <soren@stoutner.com> Tue, 12 Sep 2023 09:51:54 -0700 + electrum (4.3.4+dfsg1-1) unstable; urgency=high * Team upload diff -Nru electrum-4.3.4+dfsg1/debian/control electrum-4.3.4+dfsg1/debian/control --- electrum-4.3.4+dfsg1/debian/control 2023-02-02 15:52:07.000000000 -0700 +++ electrum-4.3.4+dfsg1/debian/control 2023-09-12 09:51:54.000000000 -0700 @@ -1,6 +1,6 @@ Source: electrum Maintainer: Debian Cryptocoin Team <team+cryptocoin@tracker.debian.org> -Uploaders: Tristan Seligmann <mithrandi@debian.org> +Uploaders: Soren Stoutner <soren@stoutner.com> Section: utils Priority: optional # libsecp256k1-1: Used to speed up elliptic curve operations. diff -Nru electrum-4.3.4+dfsg1/debian/patches/Lightning-security-fix.patch electrum-4.3.4+dfsg1/debian/patches/Lightning-security-fix.patch --- electrum-4.3.4+dfsg1/debian/patches/Lightning-security-fix.patch 1969-12-31 17:00:00.000000000 -0700 +++ electrum-4.3.4+dfsg1/debian/patches/Lightning-security-fix.patch 2023-09-06 10:41:21.000000000 -0700 @@ -0,0 +1,248 @@ +Description: Patch the Lightning security issue fixed upstream in 4.4.6. +From: Soren Stoutner <soren@stoutner.com> +Forwarded: https://github.com/spesmilo/electrum/issues/8588 +Last-Update: 2023-09-06 + +--- a/electrum/lnpeer.py ++++ b/electrum/lnpeer.py +@@ -1773,13 +1773,25 @@ + payment_secret_from_onion = None + + if total_msat > amt_to_forward: +- mpp_status = self.lnworker.check_received_mpp_htlc(payment_secret_from_onion, chan.short_channel_id, htlc, total_msat) +- if mpp_status is None: ++ from .lnworker import RecvMPPResolution ++ mpp_resolution = self.lnworker.check_mpp_status( ++ payment_secret=payment_secret_from_onion, ++ short_channel_id=chan.short_channel_id, ++ htlc=htlc, ++ expected_msat=total_msat, ++ ) ++ if mpp_resolution == RecvMPPResolution.WAITING: + return None, None +- if mpp_status is False: ++ elif mpp_resolution == RecvMPPResolution.EXPIRED: + log_fail_reason(f"MPP_TIMEOUT") + raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') +- assert mpp_status is True ++ elif mpp_resolution == RecvMPPResolution.FAILED: ++ log_fail_reason(f"mpp_resolution is FAILED") ++ raise exc_incorrect_or_unknown_pd ++ elif mpp_resolution == RecvMPPResolution.ACCEPTED: ++ pass # continue ++ else: ++ raise Exception(f"unexpected {mpp_resolution=}") + + # if there is a trampoline_onion, maybe_fulfill_htlc will be called again + if processed_onion.trampoline_onion_packet: +--- a/electrum/lnworker.py ++++ b/electrum/lnworker.py +@@ -8,7 +8,8 @@ + import random + import time + import operator +-from enum import IntEnum ++import enum ++from enum import IntEnum, Enum + from typing import (Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, + NamedTuple, Union, Mapping, Any, Iterable, AsyncGenerator, DefaultDict) + import threading +@@ -166,6 +167,19 @@ + status: int + + ++class RecvMPPResolution(Enum): ++ WAITING = enum.auto() ++ EXPIRED = enum.auto() ++ ACCEPTED = enum.auto() ++ FAILED = enum.auto() ++ ++ ++class ReceivedMPPStatus(NamedTuple): ++ resolution: RecvMPPResolution ++ expected_msat: int ++ htlc_set: Set[Tuple[ShortChannelID, UpdateAddHtlc]] ++ ++ + class ErrorAddingPeer(Exception): pass + + +@@ -653,8 +667,8 @@ + + self.sent_htlcs = defaultdict(asyncio.Queue) # type: Dict[bytes, asyncio.Queue[HtlcLog]] + self.sent_htlcs_info = dict() # (RHASH, scid, htlc_id) -> route, payment_secret, amount_msat, bucket_msat, trampoline_fee_level +- self.sent_buckets = dict() # payment_secret -> (amount_sent, amount_failed) +- self.received_mpp_htlcs = dict() # RHASH -> mpp_status, htlc_set ++ self.sent_buckets = dict() # payment_key -> (amount_sent, amount_failed) ++ self.received_mpp_htlcs = dict() # type: Dict[bytes, ReceivedMPPStatus] # payment_key -> ReceivedMPPStatus + + self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self) + # detect inflight payments +@@ -1377,13 +1391,14 @@ + + key = (payment_hash, short_channel_id, htlc.htlc_id) + self.sent_htlcs_info[key] = route, payment_secret, amount_msat, total_msat, amount_receiver_msat, trampoline_fee_level, trampoline_route ++ payment_key = payment_hash + payment_secret + # if we sent MPP to a trampoline, add item to sent_buckets + if self.uses_trampoline() and amount_msat != total_msat: +- if payment_secret not in self.sent_buckets: +- self.sent_buckets[payment_secret] = (0, 0) +- amount_sent, amount_failed = self.sent_buckets[payment_secret] ++ if payment_key not in self.sent_buckets: ++ self.sent_buckets[payment_key] = (0, 0) ++ amount_sent, amount_failed = self.sent_buckets[payment_key] + amount_sent += amount_receiver_msat +- self.sent_buckets[payment_secret] = amount_sent, amount_failed ++ self.sent_buckets[payment_key] = amount_sent, amount_failed + if self.network.path_finder: + # add inflight htlcs to liquidity hints + self.network.path_finder.update_inflight_htlcs(route, add_htlcs=True) +@@ -1857,33 +1872,91 @@ + if write_to_disk: + self.wallet.save_db() + +- def check_received_mpp_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddHtlc, expected_msat: int) -> Optional[bool]: +- """ return MPP status: True (accepted), False (expired) or None """ ++ def check_mpp_status( ++ self, *, ++ payment_secret: bytes, ++ short_channel_id: ShortChannelID, ++ htlc: UpdateAddHtlc, ++ expected_msat: int, ++ ) -> RecvMPPResolution: + payment_hash = htlc.payment_hash +- is_expired, is_accepted, htlc_set = self.received_mpp_htlcs.get(payment_secret, (False, False, set())) +- if self.get_payment_status(payment_hash) == PR_PAID: +- # payment_status is persisted +- is_accepted = True +- is_expired = False +- key = (short_channel_id, htlc) +- if key not in htlc_set: +- htlc_set.add(key) +- if not is_accepted and not is_expired: +- total = sum([_htlc.amount_msat for scid, _htlc in htlc_set]) +- first_timestamp = min([_htlc.timestamp for scid, _htlc in htlc_set]) +- if self.stopping_soon: +- is_expired = True # try to time out pending HTLCs before shutting down ++ payment_key = payment_hash + payment_secret ++ self.update_mpp_with_received_htlc( ++ payment_key=payment_key, scid=short_channel_id, htlc=htlc, expected_msat=expected_msat) ++ mpp_resolution = self.received_mpp_htlcs[payment_key].resolution ++ if mpp_resolution == RecvMPPResolution.WAITING: ++ first_timestamp = self.get_first_timestamp_of_mpp(payment_key) ++ if self.get_payment_status(payment_hash) == PR_PAID: ++ mpp_resolution = RecvMPPResolution.ACCEPTED ++ elif self.stopping_soon: ++ # try to time out pending HTLCs before shutting down ++ mpp_resolution = RecvMPPResolution.EXPIRED ++ elif self.is_mpp_amount_reached(payment_key): ++ mpp_resolution = RecvMPPResolution.ACCEPTED + elif time.time() - first_timestamp > self.MPP_EXPIRY: +- is_expired = True +- elif total == expected_msat: +- is_accepted = True +- if is_accepted or is_expired: +- htlc_set.remove(key) +- if len(htlc_set) > 0: +- self.received_mpp_htlcs[payment_secret] = is_expired, is_accepted, htlc_set +- elif payment_secret in self.received_mpp_htlcs: +- self.received_mpp_htlcs.pop(payment_secret) +- return True if is_accepted else (False if is_expired else None) ++ mpp_resolution = RecvMPPResolution.EXPIRED ++ ++ if mpp_resolution != RecvMPPResolution.WAITING: ++ self.set_mpp_resolution(payment_key=payment_key, resolution=mpp_resolution) ++ ++ self.maybe_cleanup_mpp_status(payment_key, short_channel_id, htlc) ++ return mpp_resolution ++ ++ def update_mpp_with_received_htlc( ++ self, ++ *, ++ payment_key: bytes, ++ scid: ShortChannelID, ++ htlc: UpdateAddHtlc, ++ expected_msat: int, ++ ): ++ # add new htlc to set ++ mpp_status = self.received_mpp_htlcs.get(payment_key) ++ if mpp_status is None: ++ mpp_status = ReceivedMPPStatus( ++ resolution=RecvMPPResolution.WAITING, ++ expected_msat=expected_msat, ++ htlc_set=set(), ++ ) ++ if expected_msat != mpp_status.expected_msat: ++ self.logger.info( ++ f"marking received mpp as failed. inconsistent total_msats in bucket. {payment_key.hex()=}") ++ mpp_status = mpp_status._replace(resolution=RecvMPPResolution.FAILED) ++ key = (scid, htlc) ++ if key not in mpp_status.htlc_set: ++ mpp_status.htlc_set.add(key) # side-effecting htlc_set ++ self.received_mpp_htlcs[payment_key] = mpp_status ++ ++ def set_mpp_resolution(self, *, payment_key: bytes, resolution: RecvMPPResolution): ++ mpp_status = self.received_mpp_htlcs[payment_key] ++ self.received_mpp_htlcs[payment_key] = mpp_status._replace(resolution=resolution) ++ ++ def is_mpp_amount_reached(self, payment_key: bytes) -> bool: ++ mpp_status = self.received_mpp_htlcs.get(payment_key) ++ if not mpp_status: ++ return False ++ total = sum([_htlc.amount_msat for scid, _htlc in mpp_status.htlc_set]) ++ return total >= mpp_status.expected_msat ++ ++ def get_first_timestamp_of_mpp(self, payment_key: bytes) -> int: ++ mpp_status = self.received_mpp_htlcs.get(payment_key) ++ if not mpp_status: ++ return int(time.time()) ++ return min([_htlc.timestamp for scid, _htlc in mpp_status.htlc_set]) ++ ++ def maybe_cleanup_mpp_status( ++ self, ++ payment_key: bytes, ++ short_channel_id: ShortChannelID, ++ htlc: UpdateAddHtlc, ++ ) -> None: ++ mpp_status = self.received_mpp_htlcs[payment_key] ++ if mpp_status.resolution == RecvMPPResolution.WAITING: ++ return ++ key = (short_channel_id, htlc) ++ mpp_status.htlc_set.remove(key) # side-effecting htlc_set ++ if not mpp_status.htlc_set and payment_key in self.received_mpp_htlcs: ++ self.received_mpp_htlcs.pop(payment_key) + + def get_payment_status(self, payment_hash: bytes) -> int: + info = self.get_payment_info(payment_hash) +@@ -1988,10 +2061,11 @@ + self.logger.info(f"htlc_failed {failure_message}") + + # check sent_buckets if we use trampoline +- if self.uses_trampoline() and payment_secret in self.sent_buckets: +- amount_sent, amount_failed = self.sent_buckets[payment_secret] ++ payment_key = payment_hash + payment_secret ++ if self.uses_trampoline() and payment_key in self.sent_buckets: ++ amount_sent, amount_failed = self.sent_buckets[payment_key] + amount_failed += amount_receiver_msat +- self.sent_buckets[payment_secret] = amount_sent, amount_failed ++ self.sent_buckets[payment_key] = amount_sent, amount_failed + if amount_sent != amount_failed: + self.logger.info('bucket still active...') + return +--- a/electrum/__init__.py ++++ b/electrum/__init__.py +@@ -32,3 +32,14 @@ + + + __version__ = ELECTRUM_VERSION ++ ++ ++# Ensure that asserts are enabled. For sanity and paranoia, we require this. ++# Code *should not rely* on asserts being enabled. In particular, safety and security checks should ++# always explicitly raise exceptions. However, this rule is mistakenly broken occasionally... ++try: ++ assert False # noqa: B011 ++except AssertionError: ++ pass ++else: ++ raise ImportError("Running with asserts disabled. Refusing to continue. Exiting...") diff -Nru electrum-4.3.4+dfsg1/debian/patches/series electrum-4.3.4+dfsg1/debian/patches/series --- electrum-4.3.4+dfsg1/debian/patches/series 2023-02-02 15:52:07.000000000 -0700 +++ electrum-4.3.4+dfsg1/debian/patches/series 2023-09-06 10:35:47.000000000 -0700 @@ -1,3 +1,6 @@ +# Patch Lightning security vulnerability fixed in 4.4.6 upstream. +Lightning-security-fix.patch + # This patch makes the error message make more sense on a Debian system. Improve-message-about-PyQt5.patch
Attachment:
signature.asc
Description: This is a digitally signed message part.