diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 6b9a0b3173..541d621e88 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -18,6 +18,10 @@ Node operators should watch this file very regularly. An event is also sent to the event stream for every such notification. This lets plugins notify the node operator via external systems (push notifications, email, etc). +### Initial support for onion messages + +Eclair now supports the feature `option_onion_messages`. If this feature is enabled, eclair will relay onion messages, initiating or receiving onion messages is not supported yet. + ### API changes #### Timestamps diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 3ee73e2c7f..fef81d4392 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -56,6 +56,7 @@ eclair { option_anchor_outputs = disabled option_anchors_zero_fee_htlc_tx = disabled option_shutdown_anysegwit = optional + option_onion_messages = disabled trampoline_payment = disabled keysend = disabled } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 5dba8a4a7e..4c392d5642 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -203,6 +203,11 @@ object Features { val mandatory = 26 } + case object OnionMessages extends Feature { + val rfcName = "option_onion_messages" + val mandatory = 38 + } + // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) // We're not advertising these bits yet in our announcements, clients have to assume support. // This is why we haven't added them yet to `areSupported`. @@ -231,6 +236,7 @@ object Features { AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, ShutdownAnySegwit, + OnionMessages, KeySend ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index bc182c860c..d017da0647 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -162,12 +162,12 @@ object Sphinx extends Logging { * failure messages upstream. * or a BadOnion error containing the hash of the invalid onion. */ - def peel(privateKey: PrivateKey, associatedData: ByteVector, packet: OnionRoutingPacket): Either[BadOnion, DecryptedPacket] = packet.version match { + def peel(privateKey: PrivateKey, associatedData: Option[ByteVector32], packet: OnionRoutingPacket): Either[BadOnion, DecryptedPacket] = packet.version match { case 0 => Try(PublicKey(packet.publicKey, checkValid = true)) match { case Success(packetEphKey) => val sharedSecret = computeSharedSecret(packetEphKey, privateKey) val mu = generateKey("mu", sharedSecret) - val check = mac(mu, packet.payload ++ associatedData) + val check = mac(mu, associatedData.map(packet.payload ++ _).getOrElse(packet.payload)) if (check == packet.hmac) { val rho = generateKey("rho", sharedSecret) // Since we don't know the length of the per-hop payload (we will learn it once we decode the first bytes), @@ -208,7 +208,7 @@ object Sphinx extends Logging { * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet. * @return the next packet. */ - def wrap(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Either[ByteVector, OnionRoutingPacket], onionPayloadFiller: ByteVector = ByteVector.empty): OnionRoutingPacket = { + def wrap(payload: ByteVector, associatedData: Option[ByteVector32], ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Either[ByteVector, OnionRoutingPacket], onionPayloadFiller: ByteVector = ByteVector.empty): OnionRoutingPacket = { val packetPayloadLength = packet match { case Left(startingBytes) => startingBytes.length.toInt case Right(p) => p.payload.length.toInt @@ -226,7 +226,7 @@ object Sphinx extends Logging { onionPayload2.dropRight(onionPayloadFiller.length) ++ onionPayloadFiller } - val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload ++ associatedData) + val nextHmac = mac(generateKey("mu", sharedSecret), associatedData.map(nextOnionPayload ++ _).getOrElse(nextOnionPayload)) val nextPacket = OnionRoutingPacket(Version, ephemeralPublicKey.value, nextOnionPayload, nextHmac) nextPacket } @@ -242,7 +242,7 @@ object Sphinx extends Logging { * @return An onion packet with all shared secrets. The onion packet can be sent to the first node in the list, and * the shared secrets (one per node) can be used to parse returned failure messages if needed. */ - def create(sessionKey: PrivateKey, packetPayloadLength: Int, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { + def create(sessionKey: PrivateKey, packetPayloadLength: Int, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: Option[ByteVector32]): PacketAndSecrets = { require(payloads.map(_.length + MacLength).sum <= packetPayloadLength, s"packet per-hop payloads cannot exceed $packetPayloadLength bytes") val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = generateFiller("rho", packetPayloadLength, sharedsecrets.dropRight(1), payloads.dropRight(1)) @@ -374,12 +374,17 @@ object Sphinx extends Logging { case class BlindedNode(blindedPublicKey: PublicKey, encryptedPayload: ByteVector) /** - * @param introductionNode the first node should not be blinded, otherwise the sender cannot locate it. - * @param blindedNodes blinded nodes (not including the introduction node). + * @param introductionNodeId the first node, not blinded so that the sender can locate it. + * @param blindingKey blinding tweak that can be used by the introduction node to derive the private key that + * matches the blinded public key. + * @param blindedNodes blinded nodes (including the introduction node). */ - case class BlindedRoute(introductionNode: IntroductionNode, blindedNodes: Seq[BlindedNode]) { - val nodeIds: Seq[PublicKey] = introductionNode.publicKey +: blindedNodes.map(_.blindedPublicKey) - val encryptedPayloads: Seq[ByteVector] = introductionNode.encryptedPayload +: blindedNodes.map(_.encryptedPayload) + case class BlindedRoute(introductionNodeId: PublicKey, blindingKey: PublicKey, blindedNodes: Seq[BlindedNode]) { + require(blindedNodes.nonEmpty, "blinded route must not be empty") + val introductionNode: IntroductionNode = IntroductionNode(introductionNodeId, blindedNodes.head.blindedPublicKey, blindingKey, blindedNodes.head.encryptedPayload) + val subsequentNodes: Seq[BlindedNode] = blindedNodes.tail + val blindedNodeIds: Seq[PublicKey] = blindedNodes.map(_.blindedPublicKey) + val encryptedPayloads: Seq[ByteVector] = blindedNodes.map(_.encryptedPayload) } /** @@ -402,8 +407,7 @@ object Sphinx extends Logging { e = e.multiply(PrivateKey(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes))) (BlindedNode(blindedPublicKey, encryptedPayload ++ mac), blindingKey) }.unzip - val introductionNode = IntroductionNode(publicKeys.head, blindedHops.head.blindedPublicKey, blindingKeys.head, blindedHops.head.encryptedPayload) - BlindedRoute(introductionNode, blindedHops.tail) + BlindedRoute(publicKeys.head, blindingKeys.head, blindedHops) } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 65d9e98e35..7b797c7e92 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -33,9 +33,10 @@ import fr.acinq.eclair.blockchain.{OnChainAddressGenerator, OnChainChannelFunder import fr.acinq.eclair.channel._ import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.io.PeerConnection.KillReason +import fr.acinq.eclair.message.OnionMessages import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.wire.protocol -import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, RoutingMessage, UnknownMessage, Warning} +import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, OnionMessage, RoutingMessage, UnknownMessage, Warning} import scodec.bits.ByteVector import java.net.InetSocketAddress @@ -65,7 +66,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA FinalChannelId(state.channelId) -> channel }.toMap - goto(DISCONNECTED) using DisconnectedData(channels) // when we restart, we will attempt to reconnect right away, but then we'll wait + goto(DISCONNECTED) using DisconnectedData(channels, None) // when we restart, we will attempt to reconnect right away, but then we'll wait } when(DISCONNECTED) { @@ -74,6 +75,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA stay() case Event(connectionReady: PeerConnection.ConnectionReady, d: DisconnectedData) => + d.messageToRelay.foreach(msg => self ! Peer.SendOnionMessage(remoteNodeId, msg)) gotoConnected(connectionReady, d.channels.map { case (k: ChannelId, v) => (k, v) }) case Event(Terminated(actor), d: DisconnectedData) if d.channels.exists(_._2 == actor) => @@ -94,6 +96,19 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA stay() using d.copy(channels = d.channels + (FinalChannelId(channelId) -> channel)) case Event(_: LightningMessage, _) => stay() // we probably just got disconnected and that's the last messages we received + + case Event(Peer.SendOnionMessage(toNodeId, msg), d: DisconnectedData) if toNodeId == remoteNodeId => + // We may drop a previous messageToRelay but that's fine. If we receive several messages to relay while trying to connect, we're probably getting spammed. + stay() using d.copy(messageToRelay = Some(msg)) + + case Event(_ : PeerConnection.ConnectionResult.Failure, d: DisconnectedData) => + if (d.channels.isEmpty) { + // we don't have any channel with this peer and we can't connect to it so we just drop it + stopPeer() + } else { + stay() using d.copy(messageToRelay = None) + } + } when(CONNECTED) { @@ -223,7 +238,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA stopPeer() } else { d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) - goto(DISCONNECTED) using DisconnectedData(d.channels.collect { case (k: FinalChannelId, v) => (k, v) }) + goto(DISCONNECTED) using DisconnectedData(d.channels.collect { case (k: FinalChannelId, v) => (k, v) }, None) } case Event(Terminated(actor), d: ConnectedData) if d.channels.values.toSet.contains(actor) => @@ -243,6 +258,20 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) gotoConnected(connectionReady, d.channels) + case Event(msg: OnionMessage, _: ConnectedData) => + if (nodeParams.features.hasFeature(Features.OnionMessages)) { + OnionMessages.process(nodeParams.privateKey, msg) match { + case OnionMessages.DropMessage(_) => () // We ignore bad messages + case OnionMessages.RelayMessage(nextNodeId, dataToRelay) => context.parent ! Peer.SendOnionMessage(nextNodeId, dataToRelay) + case OnionMessages.ReceiveMessage(_, _) => () // We only relay messages + } + } + stay() + + case Event(Peer.SendOnionMessage(toNodeId, msg), d: ConnectedData) if toNodeId == remoteNodeId => + d.peerConnection ! msg + stay() + case Event(unknownMsg: UnknownMessage, d: ConnectedData) if nodeParams.pluginMessageTags.contains(unknownMsg.tag) => context.system.eventStream.publish(UnknownMessageReceived(self, remoteNodeId, unknownMsg, d.connectionInfo)) stay() @@ -407,10 +436,14 @@ object Peer { def channels: Map[_ <: ChannelId, ActorRef] // will be overridden by Map[FinalChannelId, ActorRef] or Map[ChannelId, ActorRef] } case object Nothing extends Data { override def channels = Map.empty } - case class DisconnectedData(channels: Map[FinalChannelId, ActorRef]) extends Data + + case class DisconnectedData(channels: Map[FinalChannelId, ActorRef], messageToRelay: Option[OnionMessage]) extends Data + case class ConnectedData(address: InetSocketAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, channels: Map[ChannelId, ActorRef]) extends Data { val connectionInfo: ConnectionInfo = ConnectionInfo(address, peerConnection, localInit, remoteInit) + def localFeatures: Features = localInit.features + def remoteFeatures: Features = remoteInit.features } @@ -427,6 +460,8 @@ object Peer { def apply(uri: NodeURI): Connect = new Connect(uri.nodeId, Some(uri.address)) } + case class SendOnionMessage(nodeId: PublicKey, message: OnionMessage) extends PossiblyHarmful + case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful { require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index d1f13b7f25..282cb4564e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -71,6 +71,11 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) case None => sender() ! Status.Failure(new RuntimeException("peer not found")) } + case s@Peer.SendOnionMessage(nodeId, _) => + val peer = createOrGetPeer(nodeId, offlineChannels = Set.empty) + peer.tell(Peer.Connect(nodeId, None), peer) + peer forward s + case o: Peer.OpenChannel => getPeer(o.remoteNodeId) match { case Some(peer) => peer forward o diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala new file mode 100644 index 0000000000..ef6b10ae88 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala @@ -0,0 +1,129 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.message + +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.wire.protocol.MessageOnion.{BlindedFinalPayload, BlindedRelayPayload, FinalPayload, RelayPayload} +import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.EncryptedData +import fr.acinq.eclair.wire.protocol._ +import scodec.bits.ByteVector +import scodec.{Attempt, DecodeResult} + +import scala.util.{Failure, Success} + +object OnionMessages { + + case class IntermediateNode(nodeId: PublicKey, padding: Option[ByteVector] = None) + + case class Recipient(nodeId: PublicKey, pathId: Option[ByteVector], padding: Option[ByteVector] = None) + + def buildRoute(blindingSecret: PrivateKey, + intermediateNodes: Seq[IntermediateNode], + destination: Either[Recipient, Sphinx.RouteBlinding.BlindedRoute]): Sphinx.RouteBlinding.BlindedRoute = { + val last = destination match { + case Left(Recipient(nodeId, _, _)) => EncryptedRecipientDataTlv.OutgoingNodeId(nodeId) :: Nil + case Right(Sphinx.RouteBlinding.BlindedRoute(nodeId, blindingKey, _)) => EncryptedRecipientDataTlv.OutgoingNodeId(nodeId) :: EncryptedRecipientDataTlv.NextBlinding(blindingKey) :: Nil + } + val intermediatePayloads = + if (intermediateNodes.isEmpty) { + Nil + } else { + (intermediateNodes.tail.map(node => EncryptedRecipientDataTlv.OutgoingNodeId(node.nodeId) :: Nil) :+ last) + .zip(intermediateNodes).map { case (tlvs, hop) => hop.padding.map(EncryptedRecipientDataTlv.Padding(_) :: Nil).getOrElse(Nil) ++ tlvs } + .map(tlvs => BlindedRelayPayload(TlvStream(tlvs))) + .map(MessageOnionCodecs.blindedRelayPayloadCodec.encode(_).require.bytes) + } + destination match { + case Left(Recipient(nodeId, pathId, padding)) => + val tlvs = padding.map(EncryptedRecipientDataTlv.Padding(_) :: Nil).getOrElse(Nil) ++ pathId.map(EncryptedRecipientDataTlv.PathId(_) :: Nil).getOrElse(Nil) + val lastPayload = MessageOnionCodecs.finalBlindedTlvCodec.encode(BlindedFinalPayload(TlvStream(tlvs))).require.bytes + Sphinx.RouteBlinding.create(blindingSecret, intermediateNodes.map(_.nodeId) :+ nodeId, intermediatePayloads :+ lastPayload) + case Right(route) => + val Sphinx.RouteBlinding.BlindedRoute(introductionNodeId, blindingKey, blindedNodes) = Sphinx.RouteBlinding.create(blindingSecret, intermediateNodes.map(_.nodeId), intermediatePayloads) + Sphinx.RouteBlinding.BlindedRoute(introductionNodeId, blindingKey, blindedNodes ++ route.blindedNodes) + } + } + + def buildMessage(sessionKey: PrivateKey, + blindingSecret: PrivateKey, + intermediateNodes: Seq[IntermediateNode], + destination: Either[Recipient, Sphinx.RouteBlinding.BlindedRoute], + content: List[OnionMessagePayloadTlv]): (PublicKey, OnionMessage) = { + val route = buildRoute(blindingSecret, intermediateNodes, destination) + val lastPayload = MessageOnionCodecs.finalPerHopPayloadCodec.encode(FinalPayload(TlvStream(EncryptedData(route.encryptedPayloads.last) :: content))).require.bytes + val payloads = route.encryptedPayloads.dropRight(1).map(encTlv => MessageOnionCodecs.relayPerHopPayloadCodec.encode(RelayPayload(TlvStream(EncryptedData(encTlv)))).require.bytes) :+ lastPayload + val payloadSize = payloads.map(_.length + Sphinx.MacLength).sum + val packetSize = if (payloadSize <= 1300) { + 1300 + } else if (payloadSize <= 32768) { + 32768 + } else { + payloadSize.toInt + } + val Sphinx.PacketAndSecrets(packet, _) = Sphinx.create(sessionKey, packetSize, route.blindedNodes.map(_.blindedPublicKey), payloads, None) + (route.introductionNodeId, OnionMessage(blindingSecret.publicKey, packet)) + } + + // @formatter:off + sealed trait Action + case class DropMessage(reason: DropReason) extends Action + case class RelayMessage(nextNodeId: PublicKey, dataToRelay: OnionMessage) extends Action + case class ReceiveMessage(finalPayload: FinalPayload, pathId: Option[ByteVector]) extends Action + + sealed trait DropReason + case class MessageTooLarge(size: Long) extends DropReason { override def toString = s"message too large (size=$size, max=32768)" } + case class CannotDecryptOnion(message: String) extends DropReason { override def toString = s"can't decrypt onion: $message" } + case class CannotDecodeOnion(message: String) extends DropReason { override def toString = s"can't decode onion: $message" } + case class CannotDecryptBlindedPayload(message: String) extends DropReason { override def toString = s"can't decrypt blinded payload: $message" } + case class CannotDecodeBlindedPayload(message: String) extends DropReason { override def toString = s"can't decode blinded payload: $message" } + // @formatter:on + + def process(privateKey: PrivateKey, msg: OnionMessage): Action = { + if (msg.onionRoutingPacket.payload.length > 32768) { + DropMessage(MessageTooLarge(msg.onionRoutingPacket.payload.length)) + } else { + val blindedPrivateKey = Sphinx.RouteBlinding.derivePrivateKey(privateKey, msg.blindingKey) + Sphinx.peel(blindedPrivateKey, None, msg.onionRoutingPacket) match { + case Left(err: BadOnion) => DropMessage(CannotDecryptOnion(err.message)) + case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => MessageOnionCodecs.messageOnionPerHopPayloadCodec(p.isLastPacket).decode(payload.bits) match { + case Attempt.Successful(DecodeResult(relayPayload: RelayPayload, _)) => + Sphinx.RouteBlinding.decryptPayload(privateKey, msg.blindingKey, relayPayload.encryptedData) match { + case Success((decrypted, nextBlindingKey)) => + MessageOnionCodecs.blindedRelayPayloadCodec.decode(decrypted.bits) match { + case Attempt.Successful(DecodeResult(relayNext, _)) => + val toRelay = OnionMessage(relayNext.nextBlindingOverride.getOrElse(nextBlindingKey), nextPacket) + RelayMessage(relayNext.nextNodeId, toRelay) + case Attempt.Failure(err) => DropMessage(CannotDecodeBlindedPayload(err.message)) + } + case Failure(err) => DropMessage(CannotDecryptBlindedPayload(err.getMessage)) + } + case Attempt.Successful(DecodeResult(finalPayload: FinalPayload, _)) => + Sphinx.RouteBlinding.decryptPayload(privateKey, msg.blindingKey, finalPayload.encryptedData) match { + case Success((decrypted, _)) => + MessageOnionCodecs.finalBlindedTlvCodec.decode(decrypted.bits) match { + case Attempt.Successful(DecodeResult(messageToSelf, _)) => ReceiveMessage(finalPayload, messageToSelf.pathId) + case Attempt.Failure(err) => DropMessage(CannotDecodeBlindedPayload(err.message)) + } + case Failure(err) => DropMessage(CannotDecryptBlindedPayload(err.getMessage)) + } + case Attempt.Failure(err) => DropMessage(CannotDecodeOnion(err.message)) + } + } + } + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index c8beff725c..f99fe33aa2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -56,7 +56,7 @@ object IncomingPaymentPacket { case class DecodedOnionPacket[T <: PaymentOnion.PacketType](payload: T, next: OnionRoutingPacket) private[payment] def decryptOnion[T <: PaymentOnion.PacketType](paymentHash: ByteVector32, privateKey: PrivateKey, packet: OnionRoutingPacket, perHopPayloadCodec: Boolean => Codec[T])(implicit log: LoggingAdapter): Either[FailureMessage, DecodedOnionPacket[T]] = - Sphinx.peel(privateKey, paymentHash, packet) match { + Sphinx.peel(privateKey, Some(paymentHash), packet) match { case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => perHopPayloadCodec(p.isLastPacket).decode(payload.bits) match { case Attempt.Successful(DecodeResult(perHopPayload, _)) => Right(DecodedOnionPacket(perHopPayload, nextPacket)) @@ -152,7 +152,7 @@ object OutgoingPaymentPacket { case Attempt.Successful(bitVector) => bitVector.bytes case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause") } - Sphinx.create(sessionKey, packetPayloadLength, nodes, payloadsBin, associatedData) + Sphinx.create(sessionKey, packetPayloadLength, nodes, payloadsBin, Some(associatedData)) } /** @@ -250,7 +250,7 @@ object OutgoingPaymentPacket { } def buildHtlcFailure(nodeSecret: PrivateKey, reason: Either[ByteVector, FailureMessage], add: UpdateAddHtlc): Either[CannotExtractSharedSecret, ByteVector] = { - Sphinx.peel(nodeSecret, add.paymentHash, add.onionRoutingPacket) match { + Sphinx.peel(nodeSecret, Some(add.paymentHash), add.onionRoutingPacket) match { case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => val encryptedReason = reason match { case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataTlv.scala index 0db83c8dd3..8c952f3ffd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataTlv.scala @@ -39,8 +39,13 @@ object EncryptedRecipientDataTlv { /** * The final recipient may store some data in the encrypted payload for itself to avoid storing it locally. * It can for example put a payment_hash to verify that the route is used for the correct invoice. + * It should use that field to detect when blinded routes are used outside of their intended use (malicious probing) + * and react accordingly (ignore the message or send an error depending on the use-case). */ - case class RecipientSecret(data: ByteVector) extends EncryptedRecipientDataTlv + case class PathId(data: ByteVector) extends EncryptedRecipientDataTlv + + /** Blinding override for the rest of the route. */ + case class NextBlinding(blinding: PublicKey) extends EncryptedRecipientDataTlv } @@ -49,18 +54,21 @@ object EncryptedRecipientDataCodecs { import EncryptedRecipientDataTlv._ import fr.acinq.eclair.wire.protocol.CommonCodecs.{publicKey, shortchannelid, varint, varintoverflow} import scodec.Codec + import scodec.bits.HexStringSyntax import scodec.codecs._ private val padding: Codec[Padding] = variableSizeBytesLong(varintoverflow, "padding" | bytes).as[Padding] private val outgoingChannelId: Codec[OutgoingChannelId] = variableSizeBytesLong(varintoverflow, "short_channel_id" | shortchannelid).as[OutgoingChannelId] - private val outgoingNodeId: Codec[OutgoingNodeId] = variableSizeBytesLong(varintoverflow, "node_id" | publicKey).as[OutgoingNodeId] - private val recipientSecret: Codec[RecipientSecret] = variableSizeBytesLong(varintoverflow, "recipient_secret" | bytes).as[RecipientSecret] + private val outgoingNodeId: Codec[OutgoingNodeId] = (("length" | constant(hex"21")) :: ("node_id" | publicKey)).as[OutgoingNodeId] + private val pathId: Codec[PathId] = variableSizeBytesLong(varintoverflow, "path_id" | bytes).as[PathId] + private val nextBlinding: Codec[NextBlinding] = (("length" | constant(hex"21")) :: ("blinding" | publicKey)).as[NextBlinding] private val encryptedRecipientDataTlvCodec = discriminated[EncryptedRecipientDataTlv].by(varint) .typecase(UInt64(1), padding) .typecase(UInt64(2), outgoingChannelId) .typecase(UInt64(4), outgoingNodeId) - .typecase(UInt64(6), recipientSecret) + .typecase(UInt64(6), pathId) + .typecase(UInt64(12), nextBlinding) val encryptedRecipientDataCodec: Codec[TlvStream[EncryptedRecipientDataTlv]] = TlvCodecs.tlvStream[EncryptedRecipientDataTlv](encryptedRecipientDataTlvCodec).complete diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 91f93a4319..1a282194f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -308,6 +308,11 @@ object LightningMessageCodecs { ("timestampRange" | uint32) :: ("tlvStream" | GossipTimestampFilterTlv.gossipTimestampFilterTlvCodec)).as[GossipTimestampFilter] + val onionMessageCodec: Codec[OnionMessage] = ( + ("blindingKey" | publicKey) :: + ("onionPacket" | MessageOnion.messageOnionPacketCodec) :: + ("tlvStream" | OnionMessageTlv.onionMessageTlvCodec)).as[OnionMessage] + // NB: blank lines to minimize merge conflicts // @@ -361,6 +366,7 @@ object LightningMessageCodecs { .typecase(263, queryChannelRangeCodec) .typecase(264, replyChannelRangeCodec) .typecase(265, gossipTimestampFilterCodec) + .typecase(513, onionMessageCodec) // NB: blank lines to minimize merge conflicts // diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index c7773fc716..3974851076 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -320,6 +320,8 @@ object ReplyChannelRange { case class GossipTimestampFilter(chainHash: ByteVector32, firstTimestamp: TimestampSecond, timestampRange: Long, tlvStream: TlvStream[GossipTimestampFilterTlv] = TlvStream.empty) extends RoutingMessage with HasChainHash +case class OnionMessage(blindingKey: PublicKey, onionRoutingPacket: OnionRoutingPacket, tlvStream: TlvStream[OnionMessageTlv] = TlvStream.empty) extends LightningMessage + // NB: blank lines to minimize merge conflicts // diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala new file mode 100644 index 0000000000..3865e2b7fd --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala @@ -0,0 +1,125 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} +import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv} +import scodec.bits.ByteVector + +/** Tlv types used inside the onion of an [[OnionMessage]]. */ +sealed trait OnionMessagePayloadTlv extends Tlv + +object OnionMessagePayloadTlv { + + /** + * Onion messages may provide a reply path, allowing the recipient to send a message back to the original sender. + * The reply path uses route blinding, which ensures that the sender doesn't leak its identity to the recipient. + */ + case class ReplyPath(blindedRoute: BlindedRoute) extends OnionMessagePayloadTlv + + /** + * Onion messages always use route blinding, even in the forward direction. + * This ensures that intermediate nodes can't know whether they're forwarding a message or its reply. + * The sender must provide some encrypted data for each intermediate node which lets them locate the next node. + */ + case class EncryptedData(data: ByteVector) extends OnionMessagePayloadTlv + +} + +object MessageOnion { + + /** Per-hop payload from an onion message (after onion decryption and decoding). */ + sealed trait PerHopPayload + + /** Per-hop payload for an intermediate node. */ + case class RelayPayload(records: TlvStream[OnionMessagePayloadTlv]) extends PerHopPayload { + val encryptedData: ByteVector = records.get[OnionMessagePayloadTlv.EncryptedData].get.data + } + + /** Content of the encrypted data of an intermediate node's per-hop payload. */ + case class BlindedRelayPayload(records: TlvStream[EncryptedRecipientDataTlv]) { + val nextNodeId: PublicKey = records.get[EncryptedRecipientDataTlv.OutgoingNodeId].get.nodeId + val nextBlindingOverride: Option[PublicKey] = records.get[EncryptedRecipientDataTlv.NextBlinding].map(_.blinding) + } + + /** Per-hop payload for a final node. */ + case class FinalPayload(records: TlvStream[OnionMessagePayloadTlv]) extends PerHopPayload { + val replyPath: Option[OnionMessagePayloadTlv.ReplyPath] = records.get[OnionMessagePayloadTlv.ReplyPath] + val encryptedData: ByteVector = records.get[OnionMessagePayloadTlv.EncryptedData].get.data + } + + /** Content of the encrypted data of a final node's per-hop payload. */ + case class BlindedFinalPayload(records: TlvStream[EncryptedRecipientDataTlv]) { + val pathId: Option[ByteVector] = records.get[EncryptedRecipientDataTlv.PathId].map(_.data) + } + +} + +object MessageOnionCodecs { + + import MessageOnion._ + import OnionMessagePayloadTlv._ + import fr.acinq.eclair.wire.protocol.CommonCodecs._ + import scodec.codecs._ + import scodec.{Attempt, Codec} + + private val replyHopCodec: Codec[BlindedNode] = (("nodeId" | publicKey) :: ("encryptedData" | variableSizeBytes(uint16, bytes))).as[BlindedNode] + + private val replyPathCodec: Codec[ReplyPath] = variableSizeBytesLong(varintoverflow, ("firstNodeId" | publicKey) :: ("blinding" | publicKey) :: ("path" | list(replyHopCodec).xmap[Seq[BlindedNode]](_.toSeq, _.toList))).as[BlindedRoute].as[ReplyPath] + + private val encryptedDataCodec: Codec[EncryptedData] = variableSizeBytesLong(varintoverflow, bytes).as[EncryptedData] + + private val onionTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint) + .typecase(UInt64(2), replyPathCodec) + .typecase(UInt64(10), encryptedDataCodec) + + val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete + + val relayPerHopPayloadCodec: Codec[RelayPayload] = perHopPayloadCodec.narrow({ + case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(10))) + case tlvs if tlvs.get[ReplyPath].nonEmpty => Attempt.failure(ForbiddenTlv(UInt64(2))) + case tlvs => Attempt.successful(RelayPayload(tlvs)) + }, { + case RelayPayload(tlvs) => tlvs + }) + + val finalPerHopPayloadCodec: Codec[FinalPayload] = perHopPayloadCodec.narrow({ + case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(10))) + case tlvs => Attempt.successful(FinalPayload(tlvs)) + }, { + case FinalPayload(tlvs) => tlvs + }) + + val blindedRelayPayloadCodec: Codec[BlindedRelayPayload] = EncryptedRecipientDataCodecs.encryptedRecipientDataCodec.narrow({ + case tlvs if tlvs.get[EncryptedRecipientDataTlv.OutgoingNodeId].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) + case tlvs if tlvs.get[EncryptedRecipientDataTlv.PathId].nonEmpty => Attempt.failure(ForbiddenTlv(UInt64(6))) + case tlvs => Attempt.successful(BlindedRelayPayload(tlvs)) + }, { + case BlindedRelayPayload(tlvs) => tlvs + }) + + val finalBlindedTlvCodec: Codec[BlindedFinalPayload] = EncryptedRecipientDataCodecs.encryptedRecipientDataCodec.narrow( + tlvs => Attempt.successful(BlindedFinalPayload(tlvs)), + { + case BlindedFinalPayload(tlvs) => tlvs + }) + + def messageOnionPerHopPayloadCodec(isLastPacket: Boolean): Codec[PerHopPayload] = if (isLastPacket) finalPerHopPayloadCodec.upcast[PerHopPayload] else relayPerHopPayloadCodec.upcast[PerHopPayload] + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessageTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessageTlv.scala new file mode 100644 index 0000000000..4e8ab13185 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessageTlv.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.eclair.wire.protocol.CommonCodecs.varint +import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream +import scodec.Codec +import scodec.codecs.discriminated + +/** + * Created by thomash on 10/09/2021. + */ + +sealed trait OnionMessageTlv extends Tlv + +object OnionMessageTlv { + val onionMessageTlvCodec: Codec[TlvStream[OnionMessageTlv]] = tlvStream(discriminated[OnionMessageTlv].by(varint)) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala index 2de6d64f27..ed40d2f0ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala @@ -40,6 +40,15 @@ object OnionRoutingCodecs { // @formatter:on } + case class ForbiddenTlv(tag: UInt64) extends Err { + // @formatter:off + val failureMessage: FailureMessage = InvalidOnionPayload(tag, 0) + override def message = failureMessage.message + override def context: List[String] = Nil + override def pushContext(ctx: String): Err = this + // @formatter:on + } + def onionRoutingPacketCodec(payloadLength: Int): Codec[OnionRoutingPacket] = ( ("version" | uint8) :: ("publicKey" | bytes(33)) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 5995136a09..e83f770da8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -308,6 +308,7 @@ object PaymentOnionCodecs { import OnionPaymentPayloadTlv._ import PaymentOnion._ + import scodec.bits.HexStringSyntax import scodec.codecs._ import scodec.{Attempt, Codec, DecodeResult, Decoder} @@ -335,9 +336,9 @@ object PaymentOnionCodecs { private val encryptedRecipientData: Codec[EncryptedRecipientData] = variableSizeBytesLong(varintoverflow, "encrypted_data" | bytes).as[EncryptedRecipientData] - private val blindingPoint: Codec[BlindingPoint] = variableSizeBytesLong(varintoverflow, "blinding_key" | publicKey).as[BlindingPoint] + private val blindingPoint: Codec[BlindingPoint] = (("length" | constant(hex"21")) :: ("blinding" | publicKey)).as[BlindingPoint] - private val outgoingNodeId: Codec[OutgoingNodeId] = variableSizeBytesLong(varintoverflow, "node_id" | publicKey).as[OutgoingNodeId] + private val outgoingNodeId: Codec[OutgoingNodeId] = (("length" | constant(hex"21")) :: ("node_id" | publicKey)).as[OutgoingNodeId] private val invoiceFeatures: Codec[InvoiceFeatures] = variableSizeBytesLong(varintoverflow, bytes).as[InvoiceFeatures] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index ec8aa4c0ed..92aac5c7b7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -368,24 +368,25 @@ class SphinxSpec extends AnyFunSuite { val sessionKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101") val blindedRoute = RouteBlinding.create(sessionKey, publicKeys, routeBlindingPayloads) assert(blindedRoute.introductionNode.publicKey === publicKeys(0)) + assert(blindedRoute.introductionNodeId === publicKeys(0)) assert(blindedRoute.introductionNode.blindedPublicKey === PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2")) assert(blindedRoute.introductionNode.blindingEphemeralKey === PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) assert(blindedRoute.introductionNode.encryptedPayload === hex"af4fbf67bd52520bdfab6a88cd4e7f22ffad08d8b153b17ff303f93fdb4712") - assert(blindedRoute.nodeIds === Seq( - publicKeys(0), + assert(blindedRoute.blindedNodeIds === Seq( + PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2"), PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), )) - assert(blindedRoute.blindedNodes.map(_.blindedPublicKey) === Seq( + assert(blindedRoute.subsequentNodes.map(_.blindedPublicKey) === Seq( PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), )) - assert(blindedRoute.encryptedPayloads === blindedRoute.introductionNode.encryptedPayload +: blindedRoute.blindedNodes.map(_.encryptedPayload)) - assert(blindedRoute.blindedNodes.map(_.encryptedPayload) === Seq( + assert(blindedRoute.encryptedPayloads === blindedRoute.introductionNode.encryptedPayload +: blindedRoute.subsequentNodes.map(_.encryptedPayload)) + assert(blindedRoute.subsequentNodes.map(_.encryptedPayload) === Seq( hex"146c9694ead7de2a54fc43e8bb927bfc377dda7ed5a2e36b327b739e368aa602e43e07e14b3d7ed493e7ea6245924d9a03d22f0fca56babd7da19f49b7", hex"8ad7d5d448f15208417a1840f82274101b3c254c24b1b49fd676fd0c4293c9aa66ed51da52579e934a869f016f213044d1b13b63bf586e9c9832106b59", hex"52a45a884542d180e76fe84fc13e71a01f65d943ff89aed29b94644a91b037b9143cfda8f1ff25ba61c37108a5ae57d9ddc5ab688ee8b2f9f6bd94522c", @@ -398,25 +399,25 @@ class SphinxSpec extends AnyFunSuite { assert(ephKey1 === PublicKey(hex"035cb4c003d58e16cc9207270b3596c2be3309eca64c36b208c946bbb599bfcad0")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey === blindedRoute.nodeIds(1)) + assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey === blindedRoute.blindedNodeIds(1)) val Success((payload1, ephKey2)) = RouteBlinding.decryptPayload(privKeys(1), ephKey1, blindedRoute.encryptedPayloads(1)) assert(payload1 === routeBlindingPayloads(1)) assert(ephKey2 === PublicKey(hex"02e105bc01a7af07074a1b0b1d9a112a1d89c6cd87cc4e2b6ba3a824731d9508bd")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(2), ephKey2).publicKey === blindedRoute.nodeIds(2)) + assert(RouteBlinding.derivePrivateKey(privKeys(2), ephKey2).publicKey === blindedRoute.blindedNodeIds(2)) val Success((payload2, ephKey3)) = RouteBlinding.decryptPayload(privKeys(2), ephKey2, blindedRoute.encryptedPayloads(2)) assert(payload2 === routeBlindingPayloads(2)) assert(ephKey3 === PublicKey(hex"0349164db5398925ef234002e62d2834da115b8eafc73436fab98ed12266e797cc")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey === blindedRoute.nodeIds(3)) + assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey === blindedRoute.blindedNodeIds(3)) val Success((payload3, ephKey4)) = RouteBlinding.decryptPayload(privKeys(3), ephKey3, blindedRoute.encryptedPayloads(3)) assert(payload3 === routeBlindingPayloads(3)) assert(ephKey4 === PublicKey(hex"020a6d1951916adcac22125063f62c35b3686f36e5db2f77073f3d35b19c7a118a")) // The last node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey === blindedRoute.nodeIds(4)) + assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey === blindedRoute.blindedNodeIds(4)) val Success((payload4, _)) = RouteBlinding.decryptPayload(privKeys(4), ephKey4, blindedRoute.encryptedPayloads(4)) assert(payload4 === routeBlindingPayloads(4)) } @@ -440,7 +441,7 @@ class SphinxSpec extends AnyFunSuite { } // The sender obtains this information (e.g. from a Bolt11 invoice) and prepends two normal hops to reach the introduction node. - val nodeIds = publicKeys.take(2) ++ blindedRoute.nodeIds + val nodeIds = publicKeys.take(2) ++ Seq(blindedRoute.introductionNodeId) ++ blindedRoute.subsequentNodes.map(_.blindedPublicKey) assert(blindedRoute.encryptedPayloads === Seq( hex"36285ee1c0b289eeedf05c9ab66a7b669d92bd3729082c1c42e443d2775b3be7de74b1c64e0cebd0e3a8cddeff2e9acb1ddb62cbb73166723cae905938", hex"14cd98e3f4f29cc7af8624250c5bd14fb5a69be8235748909e754ef43b09b1b424b1bba062d801f8648f28fb1101b9a56dcb1f69d5e1ba7fe584f6a6be", @@ -495,7 +496,7 @@ class SphinxSpec extends AnyFunSuite { val tlvs4 = PaymentOnionCodecs.tlvPerHopPayloadCodec.decode(payload4.bits).require.value assert(tlvs4.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) val Success((recipientTlvs4, _)) = EncryptedRecipientDataCodecs.decode(privKeys(4), blindingEphemeralKey2, tlvs4.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) - assert(recipientTlvs4.get[EncryptedRecipientDataTlv.RecipientSecret].map(_.data) === Some(associatedData.bytes)) + assert(recipientTlvs4.get[EncryptedRecipientDataTlv.PathId].map(_.data) === associatedData.map(_.bytes)) assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) @@ -590,5 +591,5 @@ object SphinxSpec { hex"0109000000000000000000 06204242424242424242424242424242424242424242424242424242424242424242", ) - val associatedData = ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242") + val associatedData = Some(ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index d179e37eaf..12fb8d94ee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -33,7 +33,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike private val channels = Map(Peer.FinalChannelId(randomBytes32()) -> system.deadLetters) private val PeerNothingData = Peer.Nothing - private val PeerDisconnectedData = Peer.DisconnectedData(channels) + private val PeerDisconnectedData = Peer.DisconnectedData(channels, None) private val PeerConnectedData = Peer.ConnectedData(fakeIPAddress.socketAddress, system.deadLetters, null, null, channels.map { case (k: ChannelId, v) => (k, v) }) case class FixtureParam(nodeParams: NodeParams, remoteNodeId: PublicKey, reconnectionTask: TestFSMRef[ReconnectionTask.State, ReconnectionTask.Data, ReconnectionTask], monitor: TestProbe) @@ -77,7 +77,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val peer = TestProbe() - peer.send(reconnectionTask, Peer.Transition(PeerNothingData, Peer.DisconnectedData(Map.empty))) + peer.send(reconnectionTask, Peer.Transition(PeerNothingData, Peer.DisconnectedData(Map.empty, None))) monitor.expectNoMessage() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala new file mode 100644 index 0000000000..b1e405d91a --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala @@ -0,0 +1,218 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.message + +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.PacketAndSecrets +import fr.acinq.eclair.message.OnionMessages.{IntermediateNode, Recipient} +import fr.acinq.eclair.randomKey +import fr.acinq.eclair.wire.protocol.MessageOnion.{finalBlindedTlvCodec, messageRelayPayloadCodec, relayBlindedTlvCodec} +import fr.acinq.eclair.wire.protocol.MessageTlv._ +import fr.acinq.eclair.wire.protocol.{EncryptedRecipientDataTlv, OnionMessage, TlvStream} +import org.scalatest.funsuite.AnyFunSuite +import scodec.bits.{ByteVector, HexStringSyntax} + +/** + * Created by thomash on 23/09/2021. + */ + +class OnionMessagesSpec extends AnyFunSuite { + + test("Most basic test") { + val sessionKey = randomKey() + val blindingSecret = randomKey() + val destination = randomKey() + val (nextNodeId, message) = OnionMessages.buildMessage(sessionKey, blindingSecret, Nil, Left(Recipient(destination.publicKey, None)), Nil) + assert(nextNodeId == destination.publicKey) + + OnionMessages.process(destination, message) match { + case OnionMessages.ReceiveMessage(_, _) => () + case x => fail(x.toString) + } + } + + test("Spec tests") { + val alice = PrivateKey(hex"414141414141414141414141414141414141414141414141414141414141414101") + assert(alice.publicKey == PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) + val bob = PrivateKey(hex"424242424242424242424242424242424242424242424242424242424242424201") + assert(bob.publicKey == PublicKey(hex"0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c")) + val carol = PrivateKey(hex"434343434343434343434343434343434343434343434343434343434343434301") + assert(carol.publicKey == PublicKey(hex"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007")) + val dave = PrivateKey(hex"444444444444444444444444444444444444444444444444444444444444444401") + assert(dave.publicKey == PublicKey(hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991")) + + val blindingSecret = PrivateKey(hex"050505050505050505050505050505050505050505050505050505050505050501") + assert(blindingSecret.publicKey == PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7")) + val blindingOverride = PrivateKey(hex"070707070707070707070707070707070707070707070707070707070707070701") + assert(blindingOverride.publicKey == PublicKey(hex"02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f")) + + /* + * Building the onion manually + */ + val messageForAlice = RelayBlindedTlv(TlvStream(EncryptedRecipientDataTlv.OutgoingNodeId(bob.publicKey))) + val encodedForAlice = relayBlindedTlvCodec.encode(messageForAlice).require.bytes + assert(encodedForAlice == hex"04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") + val messageForBob = RelayBlindedTlv(TlvStream(EncryptedRecipientDataTlv.OutgoingNodeId(carol.publicKey), EncryptedRecipientDataTlv.NextBlinding(blindingOverride.publicKey))) + val encodedForBob = relayBlindedTlvCodec.encode(messageForBob).require.bytes + assert(encodedForBob == hex"0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa20070c2102989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f") + val messageForCarol = RelayBlindedTlv(TlvStream(EncryptedRecipientDataTlv.Padding(hex"0000000000000000000000000000000000000000000000000000000000000000000000"), EncryptedRecipientDataTlv.OutgoingNodeId(dave.publicKey))) + val encodedForCarol = relayBlindedTlvCodec.encode(messageForCarol).require.bytes + assert(encodedForCarol == hex"012300000000000000000000000000000000000000000000000000000000000000000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991") + val messageForDave = FinalBlindedTlv(TlvStream(EncryptedRecipientDataTlv.PathId(hex"01234567"))) + val encodedForDave = finalBlindedTlvCodec.encode(messageForDave).require.bytes + assert(encodedForDave == hex"060401234567") + + // Building blinded path Carol -> Dave + val routeFromCarol = Sphinx.RouteBlinding.create(blindingOverride, carol.publicKey :: dave.publicKey :: Nil, encodedForCarol :: encodedForDave :: Nil) + + // Building blinded path Alice -> Bob + val routeToCarol = Sphinx.RouteBlinding.create(blindingSecret, alice.publicKey :: bob.publicKey :: Nil, encodedForAlice :: encodedForBob :: Nil) + + val publicKeys = routeToCarol.blindedNodes.map(_.blindedPublicKey) concat routeFromCarol.blindedNodes.map(_.blindedPublicKey) + val encryptedPayloads = routeToCarol.encryptedPayloads concat routeFromCarol.encryptedPayloads + val payloads = encryptedPayloads.map(encTlv => messageRelayPayloadCodec.encode(MessageRelayPayload(TlvStream(EncTlv(encTlv)))).require.bytes) + assert(payloads == List( + hex"350a336970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9", + hex"580a561630da85e8759b8f3b94d74a539c6f0d870a87cf03d4986175865a2985553c997b560c36613bd9184c1a6d41a37027aabdab5433009d8409a1b638eb90373778a05716af2c215b3d31db7b2c2659716e663ba3d9c909", + hex"5a0a588285acbceb37dfb38b877a888900539be656233cd74a55c55344fb068f9d8da365340d21db96fb41b76123207daeafdfb1f571e3fea07a22e10da35f03109a0380b3c69fcbed9c698086671809658761cf65ecbc3c07a2e5", + hex"180a16a20771fd5ff63f8ee26fac46c9de93cf6bd5916a928c")) + + val sessionKey = PrivateKey(hex"090909090909090909090909090909090909090909090909090909090909090901") + + val PacketAndSecrets(packet, _) = Sphinx.create(sessionKey,1300, publicKeys, payloads, None) + assert(packet.hmac == ByteVector32(hex"cb7f2799a485f0bcb8aea2a954b886d9a4bd9f850afe79bcb4fd32bd6f1146b5")) + assert(packet.publicKey == PublicKey(hex"0256b328b30c8bf5839e24058747879408bdb36241dc9c2e7c619faa12b2920967").value) + assert(packet.payload == + hex"37df67dcefdb678725cb8074d3224dfe235ba3f22f71ac8a2c9d1398b1175295b1dd3f14c02d698021e8a8856637306c6f195e01494e0a53e9bca19b1422b5a10ac1e11de893d6692e3c9a46a9590f7e1ac9838239d82e91307a25b6d5e4045174551b1c867264d3905e4f05b2e5bcfed7e7276660bf7e956bce5afa395e7e4c15883b856bc93dd9d6a968838ef51314d38dd41e5ab84b8846dca3c61d87e55780e7a7da336a965a4652263413cdef3ac7aa1585cecd2b7d72c41a11b9526aeaa8cdde73d2dcc2fa94ee18907bc3e6ae33c564395bb7a4bbe28325ccdb07503285dacf90b5e09f4e455fb42459741f9d497000298b99f1e70adc28f59a1be85a96952f27b6a6c5d6a08822b4f5cae05daa6c2ce2f8ca5fdd4e8f0df46b94791b3159fe8eace11bcf8d585f983e53873d66b3d04e4b6fbfad1632ac31edf90f948a88742df1089024591680b1d74b28e7ce5bd25e63e7ae369795dfe74c21e24b8bbf02d1f4eb8fbd86920f41d573488abe059166aabbc3be187c435423ead6a5473994e0246efe76e419893aa2d7566b2645f3496d97585de9c92b8c5a5226398cc459ce84abc02fe2b45b5ecaf21961730d4a34bbe6fdfe720e71e3d81a494c01080d8039360d534c6ee5a3c47a1874e526969add9126b30d9192f85ba45bcfd7029cc7560f0e25e14b5deaa805360c4967705e85325ac055922863470f5397e8404022488caebf9204acd6cb02a11088aebf7e497b4ff1172f0a9c6bf980914cc4eb42fc78b457add549abf1134f84922b217502938b42d10b35079f44c5168d4c3e9fe7ca8094ef72ed73ef84f1d3530b6b3545f9f4f013e7e8cbcf2619f57754a7380ce6a9532ee14c55990faa43df6c09530a314b5f4ce597f5ec9b776e8597ce258ac47dac43bd3ac9e52788ff3a66b7dc07cd1bc3e6d197339d85fa8d3d6c3054dd1a5e416c714b544de6eb55209e40e3cac412a51748370160d2d73b6d97abd62f7bae70df27cd199c511fa693019c5717d471e934906b98cd974fda4dd1cb5e2d721044a0be2bdf24d0971e09f2f39488fe389fc5230699b4df7cec7447e5be4ea49bd7c3fe1a5ec7358510dc1dd9c1a8da68c0863188d80549e49f7c00f57d2009b2427b2aed1569603fc247734039469f9fdf3ddd3a22fa95c5d8066a468327a02b474c9915419af82c8edc67686984767fe7885207c6820f6c2e57cb8fd0bcb9981ebc8065c74e970a5d593c3b73ee25a0877ca096a9f7edfee6d43bd817c7d415fea9abb6f206c61aa36942df9318762a76b9da26d0d41a0ae9eee042a175f82dc134bf6f2d46a218db358d6852940e6e30df4a58ac6cb409e7ce99afe1e3f42768bd617af4d0a235d0ba0dd5075f9cc091784395d30e7e42d4e006db21bea9b45d1f122b75c051e84e2281573ef54ebad053218fff0cc28ea89a06adc218d4134f407654990592e75462f5ee4a463c1e46425222d48761162da8049613cafd7ecc52ff8024e9d58512b958e3a3d12dede84e1441247700bca0f992875349448b430683c756438fd4e91f3d44f3cf624ed21f3c63cf92615ecc201d0cd3159b1b3fccd8f29d2daba9ac5ba87b1dd2f83323a2b2d3176b803ce9c7bdc4bae615925eb22a213df1eeb2f8ff95586536caf042d565984aacf1425a120a5d8d7a9cbb70bf4852e116b89ff5b198d672220af2be4246372e7c3836cf50d732212a3e3346ff92873ace57fa687b2b1aab3e8dc6cb9f93f865d998cff0a1680d9012a9597c90a070e525f66226cc287814f4ac4157b15a0b25aa110946cd69fd404fafd5656669bfd1d9e509eabc004c5a" + ) + val onionForAlice = OnionMessage(blindingSecret.publicKey, packet) + + /* + * Building the onion with functions from `OnionMessages` + */ + val replyPath = OnionMessages.buildRoute(blindingOverride, IntermediateNode(carol.publicKey, padding = Some(hex"0000000000000000000000000000000000000000000000000000000000000000000000")) :: Nil, Left(Recipient(dave.publicKey, secret = Some(hex"01234567")))) + assert(replyPath == routeFromCarol) + val (_, message) = OnionMessages.buildMessage(sessionKey, blindingSecret, IntermediateNode(alice.publicKey) :: IntermediateNode(bob.publicKey) :: Nil, Right(replyPath), Nil) + assert(message == onionForAlice) + + /* + * Checking that the onion is relayed properly + */ + OnionMessages.process(alice, onionForAlice) match { + case OnionMessages.RelayMessage(nextNodeId, onionForBob) => + assert(nextNodeId == bob.publicKey) + OnionMessages.process(bob, onionForBob) match { + case OnionMessages.RelayMessage(nextNodeId, onionForCarol) => + assert(nextNodeId == carol.publicKey) + OnionMessages.process(carol, onionForCarol) match { + case OnionMessages.RelayMessage(nextNodeId, onionForDave) => + assert(nextNodeId == dave.publicKey) + OnionMessages.process(dave, onionForDave) match { + case OnionMessages.ReceiveMessage(_, _) => () + case x => fail(x.toString) + } + case x => fail(x.toString) + } + case x => fail(x.toString) + } + case x => fail(x.toString) + } + } + + test("Simple enctlv for Alice, next is Bob") { + val nodePrivateKey = PrivateKey(hex"414141414141414141414141414141414141414141414141414141414141414101") + val nodeId = PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619") + assert(nodePrivateKey.publicKey == nodeId) + val blindingSecret = PrivateKey(hex"050505050505050505050505050505050505050505050505050505050505050501") + val blindingKey = PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7") + assert(blindingSecret.publicKey == blindingKey) + val sharedSecret = ByteVector32(hex"2e83e9bc7821d3f6cec7301fa8493aee407557624fb5745bede9084852430e3f") + assert(Sphinx.computeSharedSecret(nodeId, blindingSecret) == sharedSecret) + assert(Sphinx.computeSharedSecret(blindingKey, nodePrivateKey) == sharedSecret) + assert(Sphinx.mac(ByteVector("blinded_node_id".getBytes), sharedSecret) == ByteVector32(hex"7d846b3445621d49a665e5698c52141e9dda8fa2fe0c3da7e0f9008ccc588a38")) + val blindedNodeId = PublicKey(hex"02004b5662061e9db495a6ad112b6c4eba228a079e8e304d9df50d61043acbc014") + val nextNodeId = PublicKey(hex"0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") + val encmsg = hex"04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c" + val Sphinx.RouteBlinding.BlindedRoute(_, _, blindedNodes) = Sphinx.RouteBlinding.create(blindingSecret, nodeId :: Nil, encmsg :: Nil) + assert(blindedNodes.head.blindedPublicKey == blindedNodeId) + assert(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes) == ByteVector32(hex"bae3d9ea2b06efd1b7b9b49b6cdcaad0e789474a6939ffa54ff5ec9224d5b76c")) + val enctlv = hex"6970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9" + assert(blindedNodes.head.encryptedPayload == enctlv) + val message = RelayBlindedTlv(TlvStream(EncryptedRecipientDataTlv.OutgoingNodeId(nextNodeId))) + assert(relayBlindedTlvCodec.encode(message).require.bytes == encmsg) + val relayNext = relayBlindedTlvCodec.decode(encmsg.bits).require.value + assert(relayNext.nextNodeId == nextNodeId) + assert(relayNext.nextBlinding.isEmpty) + assert(Sphinx.RouteBlinding.decryptPayload(nodePrivateKey, blindingKey, enctlv).get._1 == encmsg) + } + + test("Blinding-key-override enctlv for Bob, next is Carol") { + val nodePrivateKey = PrivateKey(hex"424242424242424242424242424242424242424242424242424242424242424201") + val nodeId = PublicKey(hex"0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") + assert(nodePrivateKey.publicKey == nodeId) + val blindingSecret = PrivateKey(hex"76d4de6c329c79623842dcf8f8eaee90c9742df1b5231f5350df4a231d16ebcf01") + val blindingKey = PublicKey(hex"03fc5e56da97b462744c9a6b0ba9d5b3ffbfb1a08367af9cc6ea5ae03c79a78eec") + assert(blindingSecret.publicKey == blindingKey) + val sharedSecret = ByteVector32(hex"f18a1ddb1cb27d8fc4faf2cf317e87524fcc6b7f053496d95bf6e6809d09851e") + assert(Sphinx.computeSharedSecret(nodeId, blindingSecret) == sharedSecret) + assert(Sphinx.computeSharedSecret(blindingKey, nodePrivateKey) == sharedSecret) + assert(Sphinx.mac(ByteVector("blinded_node_id".getBytes), sharedSecret) == ByteVector32(hex"8074773a3745818b0d97dd875023486cc35e7afd95f5e9ec1363f517979e8373")) + val blindedNodeId = PublicKey(hex"026ea8e36f78e038c659beba9229699796127471d9c7a24a0308533371fd63ad48") + val nextNodeId = PublicKey(hex"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007") + val encmsg = hex"0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa20070c2102989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f" + val Sphinx.RouteBlinding.BlindedRoute(_, _, blindedHops) = Sphinx.RouteBlinding.create(blindingSecret, nodeId :: Nil, encmsg :: Nil) + assert(blindedHops.head.blindedPublicKey == blindedNodeId) + assert(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes) == ByteVector32(hex"9afb8b2ebc174dcf9e270be24771da7796542398d29d4ff6a4e7b6b4b9205cfe")) + val enctlv = hex"1630da85e8759b8f3b94d74a539c6f0d870a87cf03d4986175865a2985553c997b560c36613bd9184c1a6d41a37027aabdab5433009d8409a1b638eb90373778a05716af2c215b3d31db7b2c2659716e663ba3d9c909" + assert(blindedHops.head.encryptedPayload == enctlv) + val message = RelayBlindedTlv(TlvStream(EncryptedRecipientDataTlv.OutgoingNodeId(nextNodeId), EncryptedRecipientDataTlv.NextBlinding(PrivateKey(hex"070707070707070707070707070707070707070707070707070707070707070701").publicKey))) + assert(relayBlindedTlvCodec.encode(message).require.bytes == encmsg) + val relayNext = relayBlindedTlvCodec.decode(encmsg.bits).require.value + assert(relayNext.nextNodeId == nextNodeId) + assert(relayNext.nextBlinding contains PrivateKey(hex"070707070707070707070707070707070707070707070707070707070707070701").publicKey) + assert(Sphinx.RouteBlinding.decryptPayload(nodePrivateKey, blindingKey, enctlv).get._1 == encmsg) + } + + test("Padded enctlv for Carol, next is Dave") { + val nodePrivateKey = PrivateKey(hex"434343434343434343434343434343434343434343434343434343434343434301") + val nodeId = PublicKey(hex"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007") + assert(nodePrivateKey.publicKey == nodeId) + val blindingSecret = PrivateKey(hex"070707070707070707070707070707070707070707070707070707070707070701") + val blindingKey = PublicKey(hex"02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f") + assert(blindingSecret.publicKey == blindingKey) + val sharedSecret = ByteVector32(hex"8c0f7716da996c4913d720dbf691b559a4945bf70cdd18e0b61e3e42635efc9c") + assert(Sphinx.computeSharedSecret(nodeId, blindingSecret) == sharedSecret) + assert(Sphinx.computeSharedSecret(blindingKey, nodePrivateKey) == sharedSecret) + assert(Sphinx.mac(ByteVector("blinded_node_id".getBytes), sharedSecret) == ByteVector32(hex"02afb2187075c8af51488242194b44c02624785ccd6fd43b5796c68f3025bf88")) + val blindedNodeId = PublicKey(hex"02f4f524562868a09d5f54fb956ade3fa51ef071d64d923e395cc6db5e290ec67b") + val nextNodeId = PublicKey(hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991") + val encmsg = hex"012300000000000000000000000000000000000000000000000000000000000000000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" + val Sphinx.RouteBlinding.BlindedRoute(_, _, blindedHops) = Sphinx.RouteBlinding.create(blindingSecret, nodeId :: Nil, encmsg :: Nil) + assert(blindedHops.head.blindedPublicKey == blindedNodeId) + assert(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes) == ByteVector32(hex"cc3b918cda6b1b049bdbe469c4dd952935e7c1518dd9c7ed0cd2cd5bc2742b82")) + val enctlv = hex"8285acbceb37dfb38b877a888900539be656233cd74a55c55344fb068f9d8da365340d21db96fb41b76123207daeafdfb1f571e3fea07a22e10da35f03109a0380b3c69fcbed9c698086671809658761cf65ecbc3c07a2e5" + assert(blindedHops.head.encryptedPayload == enctlv) + val message = RelayBlindedTlv(TlvStream(EncryptedRecipientDataTlv.Padding(hex"0000000000000000000000000000000000000000000000000000000000000000000000"), EncryptedRecipientDataTlv.OutgoingNodeId(nextNodeId))) + assert(relayBlindedTlvCodec.encode(message).require.bytes == encmsg) + val relayNext = relayBlindedTlvCodec.decode(encmsg.bits).require.value + assert(relayNext.nextNodeId == nextNodeId) + assert(relayNext.nextBlinding.isEmpty) + assert(Sphinx.RouteBlinding.decryptPayload(nodePrivateKey, blindingKey, enctlv).get._1 == encmsg) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 54e92cbb65..8cad0a676f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -196,7 +196,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Verify that the trampoline node can correctly peel the trampoline onion. val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, pr.paymentHash, trampolineOnion) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value assert(trampolinePayload.amountToForward === finalAmount) @@ -208,7 +208,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(trampolinePayload.invoiceFeatures === None) // Verify that the recipient can correctly peel the trampoline onion. - val Right(decrypted1) = Sphinx.peel(priv_c.privateKey, pr.paymentHash, decrypted.nextPacket) + val Right(decrypted1) = Sphinx.peel(priv_c.privateKey, Some(pr.paymentHash), decrypted.nextPacket) assert(decrypted1.isLastPacket) val finalPayload = PaymentOnionCodecs.finalPerHopPayloadCodec.decode(decrypted1.payload.bits).require.value assert(finalPayload.amount === finalAmount) @@ -235,7 +235,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Verify that the trampoline node can correctly peel the trampoline onion. val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, pr.paymentHash, trampolineOnion) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value assert(trampolinePayload.amountToForward === finalAmount) @@ -372,7 +372,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(trampolineOnion.nonEmpty) // Verify that the trampoline node can correctly peel the trampoline onion. - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, pr.paymentHash, trampolineOnion.get.packet) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion.get.packet) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value assert(trampolinePayload.amountToForward === finalAmount) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index eb5bb311fc..73d8773191 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -360,7 +360,7 @@ object PaymentPacketSpec { case Attempt.Successful(bitVector) => bitVector.bytes case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause") } - Sphinx.create(sessionKey, packetPayloadLength, nodes, payloadsBin, associatedData).packet + Sphinx.create(sessionKey, packetPayloadLength, nodes, payloadsBin, Some(associatedData)).packet } def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala index c9f9498659..989a95820f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.RouteBlinding -import fr.acinq.eclair.wire.protocol.EncryptedRecipientDataTlv.{OutgoingChannelId, OutgoingNodeId, Padding, RecipientSecret} +import fr.acinq.eclair.wire.protocol.EncryptedRecipientDataTlv._ import fr.acinq.eclair.{ShortChannelId, UInt64, randomKey} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -17,13 +17,14 @@ class EncryptedRecipientDataSpec extends AnyFunSuiteLike { val payloads = Seq( (TlvStream[EncryptedRecipientDataTlv](Padding(hex"000000"), OutgoingChannelId(ShortChannelId(561))), hex"0103000000 02080000000000000231"), (TlvStream[EncryptedRecipientDataTlv](OutgoingNodeId(PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486"))), hex"0421025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486"), - (TlvStream[EncryptedRecipientDataTlv](RecipientSecret(hex"0101010101010101010101010101010101010101010101010101010101010101")), hex"06200101010101010101010101010101010101010101010101010101010101010101"), + (TlvStream[EncryptedRecipientDataTlv](OutgoingNodeId(PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486")), NextBlinding(PublicKey(hex"027710df7a1d7ad02e3572841a829d141d9f56b17de9ea124d2f83ea687b2e0461"))), hex"0421025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486 0c21027710df7a1d7ad02e3572841a829d141d9f56b17de9ea124d2f83ea687b2e0461"), + (TlvStream[EncryptedRecipientDataTlv](PathId(hex"0101010101010101010101010101010101010101010101010101010101010101")), hex"06200101010101010101010101010101010101010101010101010101010101010101"), (TlvStream[EncryptedRecipientDataTlv](Seq(OutgoingChannelId(ShortChannelId(42))), Seq(GenericTlv(UInt64(65535), hex"06c1"))), hex"0208000000000000002a fdffff0206c1"), ) val blindedRoute = RouteBlinding.create(sessionKey, nodePrivKeys.map(_.publicKey), payloads.map(_._2)) val blinding0 = sessionKey.publicKey - val Success((decryptedPayload0, blinding1)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys.head, blinding0, blindedRoute.encryptedPayloads(0)) + val Success((decryptedPayload0, blinding1)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(0), blinding0, blindedRoute.encryptedPayloads(0)) val Success((decryptedPayload1, blinding2)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(1), blinding1, blindedRoute.encryptedPayloads(1)) val Success((decryptedPayload2, blinding3)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(2), blinding2, blindedRoute.encryptedPayloads(2)) val Success((decryptedPayload3, _)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(3), blinding3, blindedRoute.encryptedPayloads(3)) @@ -34,6 +35,8 @@ class EncryptedRecipientDataSpec extends AnyFunSuiteLike { val testCases = Seq( hex"02080000000000000231 ff", // additional trailing bytes after tlv stream hex"01040000 02080000000000000231", // invalid padding tlv + hex"0420025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce14", // invalid public key length + hex"0c20025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce14", // invalid next blinding length hex"02080000000000000231 0103000000", // invalid tlv stream ordering hex"02080000000000000231 10080000000000000231", // unknown even tlv field )