Skip to content

Commit

Permalink
Clarify route blinding types (#2059)
Browse files Browse the repository at this point in the history
We rename the EncryptedRecipientData types.
The data it contains is namespaced to usages for route blinding, so we
make that explicit.

This way if future scenarios use another kind of encrypted tlv stream
we won't have name clashes (e.g. encrypted state backup).

We also update the route blinding test vectors to the final spec version.
  • Loading branch information
t-bast authored Nov 9, 2021
1 parent 083dc3c commit 333e9ef
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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.RouteBlindingEncryptedDataTlv._
import fr.acinq.eclair.wire.protocol._
import scodec.bits.ByteVector
import scodec.{Attempt, DecodeResult}
Expand All @@ -36,21 +37,21 @@ object OnionMessages {
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
case Left(Recipient(nodeId, _, _)) => OutgoingNodeId(nodeId) :: Nil
case Right(Sphinx.RouteBlinding.BlindedRoute(nodeId, blindingKey, _)) => OutgoingNodeId(nodeId) :: 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 }
(intermediateNodes.tail.map(node => OutgoingNodeId(node.nodeId) :: Nil) :+ last)
.zip(intermediateNodes).map { case (tlvs, hop) => hop.padding.map(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 tlvs = padding.map(Padding(_) :: Nil).getOrElse(Nil) ++ pathId.map(PathId(_) :: Nil).getOrElse(Nil)
val lastPayload = MessageOnionCodecs.blindedFinalPayloadCodec.encode(BlindedFinalPayload(TlvStream(tlvs))).require.bytes
Sphinx.RouteBlinding.create(blindingSecret, intermediateNodes.map(_.nodeId) :+ nodeId, intermediatePayloads :+ lastPayload)
case Right(route) =>
Expand All @@ -61,11 +62,12 @@ object OnionMessages {

/**
* Builds an encrypted onion containing a message that should be relayed to the destination.
* @param sessionKey A random key to encrypt the onion
* @param blindingSecret A random key to encrypt the onion
*
* @param sessionKey A random key to encrypt the onion
* @param blindingSecret A random key to encrypt the onion
* @param intermediateNodes List of intermediate nodes between us and the destination, can be empty if we want to contact the destination directly
* @param destination The destination of this message, can be a node id or a blinded route
* @param content List of TLVs to send to the recipient of the message
* @param destination The destination of this message, can be a node id or a blinded route
* @param content List of TLVs to send to the recipient of the message
* @return The node id to send the onion to and the onion containing the message
*/
def buildMessage(sessionKey: PrivateKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ object MessageOnion {
}

/** 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)
case class BlindedRelayPayload(records: TlvStream[RouteBlindingEncryptedDataTlv]) {
val nextNodeId: PublicKey = records.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].get.nodeId
val nextBlindingOverride: Option[PublicKey] = records.get[RouteBlindingEncryptedDataTlv.NextBlinding].map(_.blinding)
}

/** Per-hop payload for a final node. */
Expand All @@ -65,8 +65,8 @@ object MessageOnion {
}

/** 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)
case class BlindedFinalPayload(records: TlvStream[RouteBlindingEncryptedDataTlv]) {
val pathId: Option[ByteVector] = records.get[RouteBlindingEncryptedDataTlv.PathId].map(_.data)
}

}
Expand Down Expand Up @@ -106,15 +106,15 @@ object MessageOnionCodecs {
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)))
val blindedRelayPayloadCodec: Codec[BlindedRelayPayload] = RouteBlindingEncryptedDataCodecs.encryptedDataCodec.narrow({
case tlvs if tlvs.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4)))
case tlvs if tlvs.get[RouteBlindingEncryptedDataTlv.PathId].nonEmpty => Attempt.failure(ForbiddenTlv(UInt64(6)))
case tlvs => Attempt.successful(BlindedRelayPayload(tlvs))
}, {
case BlindedRelayPayload(tlvs) => tlvs
})

val blindedFinalPayloadCodec: Codec[BlindedFinalPayload] = EncryptedRecipientDataCodecs.encryptedRecipientDataCodec.narrow(
val blindedFinalPayloadCodec: Codec[BlindedFinalPayload] = RouteBlindingEncryptedDataCodecs.encryptedDataCodec.narrow(
tlvs => Attempt.successful(BlindedFinalPayload(tlvs)),
{
case BlindedFinalPayload(tlvs) => tlvs
Expand All @@ -128,4 +128,5 @@ object MessageOnionCodecs {
("publicKey" | bytes(33)),
("onionPayload" | bytes)) ~
("hmac" | bytes32) flattenLeftPairs).as[OnionRoutingPacket]

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,45 @@
package fr.acinq.eclair.wire.protocol

import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.{ShortChannelId, UInt64}
import scodec.bits.ByteVector

import scala.util.Try

sealed trait EncryptedRecipientDataTlv extends Tlv
/**
* Created by t-bast on 19/10/2021.
*/

sealed trait RouteBlindingEncryptedDataTlv extends Tlv

object EncryptedRecipientDataTlv {
object RouteBlindingEncryptedDataTlv {

/** Some padding can be added to ensure all payloads are the same size to improve privacy. */
case class Padding(dummy: ByteVector) extends EncryptedRecipientDataTlv
case class Padding(dummy: ByteVector) extends RouteBlindingEncryptedDataTlv

/** Id of the outgoing channel, used to identify the next node. */
case class OutgoingChannelId(shortChannelId: ShortChannelId) extends EncryptedRecipientDataTlv
case class OutgoingChannelId(shortChannelId: ShortChannelId) extends RouteBlindingEncryptedDataTlv

/** Id of the next node. */
case class OutgoingNodeId(nodeId: PublicKey) extends EncryptedRecipientDataTlv
case class OutgoingNodeId(nodeId: PublicKey) extends RouteBlindingEncryptedDataTlv

/**
* 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 PathId(data: ByteVector) extends EncryptedRecipientDataTlv
case class PathId(data: ByteVector) extends RouteBlindingEncryptedDataTlv

/** Blinding override for the rest of the route. */
case class NextBlinding(blinding: PublicKey) extends EncryptedRecipientDataTlv
case class NextBlinding(blinding: PublicKey) extends RouteBlindingEncryptedDataTlv

}

object EncryptedRecipientDataCodecs {
object RouteBlindingEncryptedDataCodecs {

import EncryptedRecipientDataTlv._
import RouteBlindingEncryptedDataTlv._
import fr.acinq.eclair.wire.protocol.CommonCodecs.{publicKey, shortchannelid, varint, varintoverflow}
import scodec.Codec
import scodec.bits.HexStringSyntax
Expand All @@ -63,27 +67,27 @@ object EncryptedRecipientDataCodecs {
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)
private val encryptedDataTlvCodec = discriminated[RouteBlindingEncryptedDataTlv].by(varint)
.typecase(UInt64(1), padding)
.typecase(UInt64(2), outgoingChannelId)
.typecase(UInt64(4), outgoingNodeId)
.typecase(UInt64(6), pathId)
.typecase(UInt64(8), nextBlinding)

val encryptedRecipientDataCodec: Codec[TlvStream[EncryptedRecipientDataTlv]] = TlvCodecs.tlvStream[EncryptedRecipientDataTlv](encryptedRecipientDataTlvCodec).complete
val encryptedDataCodec: Codec[TlvStream[RouteBlindingEncryptedDataTlv]] = TlvCodecs.tlvStream[RouteBlindingEncryptedDataTlv](encryptedDataTlvCodec).complete

/**
* Decrypt and decode the contents of an encrypted_recipient_data TLV field.
*
* @param nodePrivKey this node's private key.
* @param blindingKey blinding point (usually provided in the lightning message).
* @param encryptedRecipientData encrypted recipient data (usually provided inside an onion).
* @param nodePrivKey this node's private key.
* @param blindingKey blinding point (usually provided in the lightning message).
* @param encryptedData encrypted route blinding data (usually provided inside an onion).
* @return decrypted contents of the encrypted recipient data, which usually contain information about the next node,
* and the blinding point that should be sent to the next node.
*/
def decode(nodePrivKey: PrivateKey, blindingKey: PublicKey, encryptedRecipientData: ByteVector): Try[(TlvStream[EncryptedRecipientDataTlv], PublicKey)] = {
RouteBlinding.decryptPayload(nodePrivKey, blindingKey, encryptedRecipientData).flatMap {
case (payload, nextBlindingKey) => encryptedRecipientDataCodec.decode(payload.bits).map(r => (r.value, nextBlindingKey)).toTry
def decode(nodePrivKey: PrivateKey, blindingKey: PublicKey, encryptedData: ByteVector): Try[(TlvStream[RouteBlindingEncryptedDataTlv], PublicKey)] = {
Sphinx.RouteBlinding.decryptPayload(nodePrivKey, blindingKey, encryptedData).flatMap {
case (payload, nextBlindingKey) => encryptedDataCodec.decode(payload.bits).map(r => (r.value, nextBlindingKey)).toTry
}
}

Expand Down
Loading

0 comments on commit 333e9ef

Please sign in to comment.