Skip to content

Commit

Permalink
Store blinding pubkey for introduction node (#2024)
Browse files Browse the repository at this point in the history
It doesn't make sense to throw away this information, and it's useful in
some scenarios such as onion messages.

The ephemeral keys aren't part of the route, they're usually derived hop
by hop instead. We only need to keep the first one that must be somehow
sent to the introduction node.
  • Loading branch information
t-bast committed Oct 22, 2021
1 parent 4e9190a commit 28d04ba
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 26 deletions.
26 changes: 12 additions & 14 deletions eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
Original file line number Diff line number Diff line change
Expand Up @@ -380,29 +380,27 @@ object Sphinx extends Logging {

/**
* @param publicKey introduction node's public key (which cannot be blinded since the sender need to find a route to it).
* @param blindingEphemeralKey blinding tweak that can be used by the introduction node to derive the private key that
* lets it decrypt the encrypted payload.
* @param blindedPublicKey blinded public key, which hides the real public key.
* @param blindingEphemeralKey blinding tweak that can be used by the receiving node to derive the private key that
* matches the blinded public key.
* @param encryptedPayload encrypted payload that can be decrypted with the introduction node's private key and the
* blinding ephemeral key.
*/
case class IntroductionNode(publicKey: PublicKey, blindingEphemeralKey: PublicKey, encryptedPayload: ByteVector)
case class IntroductionNode(publicKey: PublicKey, blindedPublicKey: PublicKey, blindingEphemeralKey: PublicKey, encryptedPayload: ByteVector)

/**
* @param blindedPublicKey blinded public key, which hides the real public key.
* @param blindingEphemeralKey blinding tweak that can be used by the receiving node to derive the private key that
* matches the blinded public key.
* @param encryptedPayload encrypted payload that can be decrypted with the receiving node's private key and the
* blinding ephemeral key.
* @param blindedPublicKey blinded public key, which hides the real public key.
* @param encryptedPayload encrypted payload that can be decrypted with the receiving node's private key and the
* blinding ephemeral key.
*/
case class BlindedNode(blindedPublicKey: PublicKey, blindingEphemeralKey: PublicKey, encryptedPayload: ByteVector)
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).
*/
case class BlindedRoute(introductionNode: IntroductionNode, blindedNodes: Seq[BlindedNode]) {
val nodeIds: Seq[PublicKey] = introductionNode.publicKey +: blindedNodes.map(_.blindedPublicKey)
val blindingEphemeralKeys: Seq[PublicKey] = introductionNode.blindingEphemeralKey +: blindedNodes.map(_.blindingEphemeralKey)
val encryptedPayloads: Seq[ByteVector] = introductionNode.encryptedPayload +: blindedNodes.map(_.encryptedPayload)
}

Expand All @@ -417,16 +415,16 @@ object Sphinx extends Logging {
def create(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector]): BlindedRoute = {
require(publicKeys.length == payloads.length, "a payload must be provided for each node in the blinded path")
var e = sessionKey
val blindedHops = publicKeys.zip(payloads).map { case (publicKey, payload) =>
val (blindedHops, blindingKeys) = publicKeys.zip(payloads).map { case (publicKey, payload) =>
val blindingKey = e.publicKey
val sharedSecret = computeSharedSecret(publicKey, e)
val blindedPublicKey = blind(publicKey, generateKey("blinded_node_id", sharedSecret))
val rho = generateKey("rho", sharedSecret)
val (encryptedPayload, mac) = ChaCha20Poly1305.encrypt(rho, zeroes(12), payload, ByteVector.empty)
e = e.multiply(PrivateKey(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes)))
BlindedNode(blindedPublicKey, blindingKey, encryptedPayload ++ mac)
}
val introductionNode = IntroductionNode(publicKeys.head, blindedHops.head.blindingEphemeralKey, blindedHops.head.encryptedPayload)
(BlindedNode(blindedPublicKey, encryptedPayload ++ mac), blindingKey)
}.unzip
val introductionNode = IntroductionNode(publicKeys.head, blindedHops.head.blindedPublicKey, blindingKeys.head, blindedHops.head.encryptedPayload)
BlindedRoute(introductionNode, blindedHops.tail)
}

Expand Down
18 changes: 6 additions & 12 deletions eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ class SphinxSpec extends AnyFunSuite {
val sessionKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101")
val blindedRoute = RouteBlinding.create(sessionKey, publicKeys, routeBlindingPayloads)
assert(blindedRoute.introductionNode.publicKey === publicKeys(0))
assert(blindedRoute.introductionNode.blindedPublicKey === PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2"))
assert(blindedRoute.introductionNode.blindingEphemeralKey === PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))
assert(blindedRoute.introductionNode.encryptedPayload === hex"a245b767bd52520bdf8179b2dc681d1a36c2ededaf59429dfc4bea342fa460c9")
assert(blindedRoute.nodeIds === Seq(
Expand All @@ -383,13 +384,6 @@ class SphinxSpec extends AnyFunSuite {
PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"),
PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"),
))
assert(blindedRoute.blindingEphemeralKeys === blindedRoute.introductionNode.blindingEphemeralKey +: blindedRoute.blindedNodes.map(_.blindingEphemeralKey))
assert(blindedRoute.blindedNodes.map(_.blindingEphemeralKey) === Seq(
PublicKey(hex"035cb4c003d58e16cc9207270b3596c2be3309eca64c36b208c946bbb599bfcad0"),
PublicKey(hex"02e105bc01a7af07074a1b0b1d9a112a1d89c6cd87cc4e2b6ba3a824731d9508bd"),
PublicKey(hex"0349164db5398925ef234002e62d2834da115b8eafc73436fab98ed12266e797cc"),
PublicKey(hex"020a6d1951916adcac22125063f62c35b3686f36e5db2f77073f3d35b19c7a118a"),
))
assert(blindedRoute.encryptedPayloads === blindedRoute.introductionNode.encryptedPayload +: blindedRoute.blindedNodes.map(_.encryptedPayload))
assert(blindedRoute.blindedNodes.map(_.encryptedPayload) === Seq(
hex"38748f94ead7de2a54fc43e8bb927bfc377dda7ed5a2e36b327b739c3c82a602e43e07e378f17cd46ee32d987eb8b6d03b3403acb095bd2868f640b92ea1",
Expand All @@ -399,27 +393,27 @@ class SphinxSpec extends AnyFunSuite {
))

// The introduction point can decrypt its encrypted payload and obtain the next ephemeral public key.
val Success((payload0, ephKey1)) = RouteBlinding.decryptPayload(privKeys(0), blindedRoute.blindingEphemeralKeys(0), blindedRoute.encryptedPayloads(0))
val Success((payload0, ephKey1)) = RouteBlinding.decryptPayload(privKeys(0), blindedRoute.introductionNode.blindingEphemeralKey, blindedRoute.encryptedPayloads(0))
assert(payload0 === routeBlindingPayloads(0))
assert(ephKey1 === blindedRoute.blindingEphemeralKeys(1))
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))
val Success((payload1, ephKey2)) = RouteBlinding.decryptPayload(privKeys(1), ephKey1, blindedRoute.encryptedPayloads(1))
assert(payload1 === routeBlindingPayloads(1))
assert(ephKey2 === blindedRoute.blindingEphemeralKeys(2))
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))
val Success((payload2, ephKey3)) = RouteBlinding.decryptPayload(privKeys(2), ephKey2, blindedRoute.encryptedPayloads(2))
assert(payload2 === routeBlindingPayloads(2))
assert(ephKey3 === blindedRoute.blindingEphemeralKeys(3))
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))
val Success((payload3, ephKey4)) = RouteBlinding.decryptPayload(privKeys(3), ephKey3, blindedRoute.encryptedPayloads(3))
assert(payload3 === routeBlindingPayloads(3))
assert(ephKey4 === blindedRoute.blindingEphemeralKeys(4))
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))
Expand Down

0 comments on commit 28d04ba

Please sign in to comment.