diff --git a/.gitignore b/.gitignore index 31e3ac6..033d67f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ vendor/ .idea +.aider* diff --git a/cmd/main.go b/cmd/main.go index 032738f..30ff842 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -70,6 +70,12 @@ func main() { "data.", Value: defaultHopDataPath, }, + cli.BoolFlag{ + Name: "onion-message", + Usage: "Create an onion message " + + "packet rather than a " + + "payment onion.", + }, }, }, { @@ -203,8 +209,14 @@ func generate(ctx *cli.Context) error { return fmt.Errorf("could not peel onion spec: %v", err) } + var onionOpts []sphinx.OnionPacketOption + if ctx.Bool("onion-message") { + onionOpts = append(onionOpts, sphinx.WithOnionMessage()) + } + msg, err := sphinx.NewOnionPacket( path, sessionKey, assocData, sphinx.DeterministicPacketFiller, + onionOpts..., ) if err != nil { return fmt.Errorf("error creating message: %v", err) diff --git a/go.mod b/go.mod index eae622f..a4cf852 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,31 @@ -module github.com/lightningnetwork/lightning-onion +module github.com/gijswijs/lightning-onion require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da - github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4 - github.com/btcsuite/btcd/btcec/v2 v2.1.0 + github.com/btcsuite/btcd v0.24.1-0.20240301210420-1a2b599bf1af + github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/urfave/cli v1.22.5 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/crypto v0.16.0 +) + +require ( + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/lightningnetwork/lnd/fn/v2 v2.0.2 // indirect + golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect + golang.org/x/sync v0.7.0 // indirect ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/lightningnetwork/lnd/tlv v1.3.2 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect + golang.org/x/sys v0.15.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index af52f16..2836f91 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,14 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4 h1:CEGr/598C/0LZQUoioaT6sdGGcJgu4+ck0PDeJ/QkKs= github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08= +github.com/btcsuite/btcd v0.24.1-0.20240301210420-1a2b599bf1af h1:F60A3wst4/fy9Yr1Vn8MYmFlfn7DNLxp8o8UTvhqgBE= +github.com/btcsuite/btcd v0.24.1-0.20240301210420-1a2b599bf1af/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd/btcec/v2 v2.1.0 h1:Whmbo9yShKKG+WrUfYGFfgj77vYBiwhwBSJnM66TMKI= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= @@ -15,6 +21,10 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/lightningnetwork/lnd/fn/v2 v2.0.2 h1:M7o2lYrh/zCp+lntPB3WP/rWTu5U+4ssyHW+kqNJ0fs= +github.com/lightningnetwork/lnd/fn/v2 v2.0.2/go.mod h1:TOzwrhjB/Azw1V7aa8t21ufcQmdsQOQMDtxVOQWNl8s= +github.com/lightningnetwork/lnd/tlv v1.3.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= +github.com/lightningnetwork/lnd/tlv v1.3.2/go.mod h1:pJuiBj1ecr1WWLOtcZ+2+hu9Ey25aJWFIsjmAoPPnmc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= @@ -28,16 +38,26 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= +golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/packetfiller.go b/packetfiller.go index 79c1441..f02bab5 100644 --- a/packetfiller.go +++ b/packetfiller.go @@ -12,16 +12,16 @@ import ( // in order to ensure we don't leak information on the true route length to the // receiver. The packet filler may also use the session key to generate a set // of filler bytes if it wishes to be deterministic. -type PacketFiller func(*btcec.PrivateKey, *[routingInfoSize]byte) error +type PacketFiller func(*btcec.PrivateKey, []byte) error // RandPacketFiller is a packet filler that reads a set of random bytes from a // CSPRNG. -func RandPacketFiller(_ *btcec.PrivateKey, mixHeader *[routingInfoSize]byte) error { +func RandPacketFiller(_ *btcec.PrivateKey, mixHeader []byte) error { // Read out random bytes to fill out the rest of the starting packet // after the hop payload for the final node. This mitigates a privacy // leak that may reveal a lower bound on the true path length to the // receiver. - if _, err := rand.Read(mixHeader[:]); err != nil { + if _, err := rand.Read(mixHeader); err != nil { return err } @@ -31,7 +31,7 @@ func RandPacketFiller(_ *btcec.PrivateKey, mixHeader *[routingInfoSize]byte) err // BlankPacketFiller is a packet filler that doesn't attempt to fill out the // packet at all. It should ONLY be used for generating test vectors or other // instances that required deterministic packet generation. -func BlankPacketFiller(_ *btcec.PrivateKey, _ *[routingInfoSize]byte) error { +func BlankPacketFiller(_ *btcec.PrivateKey, _ []byte) error { return nil } @@ -39,7 +39,7 @@ func BlankPacketFiller(_ *btcec.PrivateKey, _ *[routingInfoSize]byte) error { // set of filler bytes by using chacha20 with a key derived from the session // key. func DeterministicPacketFiller(sessionKey *btcec.PrivateKey, - mixHeader *[routingInfoSize]byte) error { + mixHeader []byte) error { // First, we'll generate a new key that'll be used to generate some // random bytes for our padding purposes. To derive this new key, we @@ -55,7 +55,7 @@ func DeterministicPacketFiller(sessionKey *btcec.PrivateKey, if err != nil { return err } - padCipher.XORKeyStream(mixHeader[:], mixHeader[:]) + padCipher.XORKeyStream(mixHeader, mixHeader) return nil } diff --git a/path_test.go b/path_test.go index 9a301d9..6b466ba 100644 --- a/path_test.go +++ b/path_test.go @@ -12,8 +12,17 @@ import ( ) const ( - routeBlindingTestFileName = "testdata/route-blinding-test.json" - onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json" + routeBlindingTestFileName = "testdata/route-blinding-test.json" + onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json" + blindedOnionMessageOnionTestFileName = "testdata/blinded-onion-message-onion-test.json" +) + +var ( + // bolt4PubKeys contains the public keys used in the Bolt 4 spec test + // vectors. We convert them variables named after the commonly used + // names in cryptography. + alicePubKey = bolt4PubKeys[0] + bobPubKey = bolt4PubKeys[1] ) // TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against @@ -117,6 +126,164 @@ func TestBuildBlindedRoute(t *testing.T) { } } +// TestBuildOnionMessageBlindedRoute tests the construction of a blinded route +// for an onion message, specifically the concatenation of two blinded paths, +// against the spec test vectors in `blinded-onion-message-onion-test.json`. It +// verifies the correctness of BuildBlindedPath, decryptBlindedHopData, and +// NextEphemeral. +// +// The test setup involves several parties and two distinct blinded paths that +// are combined to form the full route: +// +// 1. Path from Dave: Dave (the receiver) first constructs a blinded path for a +// message to be sent from Bob to himself (Dave). +// The path is: Bob -> Carol -> Dave +// +// 2. Path from Sender: Dave gives his blinded path to a Sender. The Sender +// then creates their own blinded path from themselves to Bob, passing +// through Alice. The path is: Sender -> Alice -> Bob +// +// 3. Path Concatenation: The Sender prepends their path to Dave's path, +// creating a final, concatenated route: +// Sender -> Alice -> Bob -> Carol -> Dave +// To link the two paths, the Sender includes a `next_path_key_override` +// in the payload for Alice. This override is set to the first path key +// (blinding point) of Dave's path, instructing Alice to use it for the next +// hop (Bob) instead of the key that she could derive herself. +// +// The test then asserts that the generated concatenated path matches the test +// vector's expected route. Finally, it simulates the decryption process at each +// hop, verifying that each node can correctly decrypt its payload and derive +// the correct next ephemeral key. +func TestBuildOnionMessageBlindedRoute(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := os.ReadFile(blindedOnionMessageOnionTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // onionMessageJsonTestCase struct defined below. + testCase := &onionMessageJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + require.Len(t, testCase.Generate.Hops, 4) + + // buildMessagePath is a helper closure used to convert + // hopOnionMessageData objects into HopInfo objects. + buildMessagePath := func(h []hopOnionMessageData, + initialHopID string) []*HopInfo { + + path := make([]*HopInfo, len(h)) + // The json test vector doesn't properly specify the current + // node id, so we need the initial Node ID as a starting point. + currentHop := initialHopID + for i, hop := range h { + nodeIDStr, err := hex.DecodeString(currentHop) + require.NoError(t, err) + nodeID, err := btcec.ParsePubKey(nodeIDStr) + require.NoError(t, err) + payload, err := hex.DecodeString(hop.EncryptedDataTlv) + require.NoError(t, err) + + path[i] = &HopInfo{ + NodePub: nodeID, + PlainText: payload, + } + + // The json test vector doesn't properly specify the + // current node id. It does specify the next node id. So + // to get the current node id for the next iteration, we + // get the next node id here. + currentHop = hop.EncodedOnionMessageTLVs.NextNodeID + } + return path + } + + // First, Dave will build a blinded path from Bob to itself. + daveSessKey := privKeyFromString( + testCase.Generate.Hops[1].PathKeySecret, + ) + daveBobPath := buildMessagePath( + testCase.Generate.Hops[1:], bobPubKey, + ) + daveBobBlindedPath, err := BuildBlindedPath(daveSessKey, daveBobPath) + require.NoError(t, err) + + // At this point, Dave will give his blinded path to the Sender who will + // then build its own blinded route from itself to Bob via Alice. The + // sender will then concatenate the two paths. Note that in the payload + // for Alice, the `next_path_key_override` field is added which is set + // to the first path key in Dave's blinded route. This will indicate to + // Alice that she should use this point for the next path key instead of + // the next path key that she derives. + // Path created by Dave: Bob -> Carol -> Dave + // Path that the Sender will build: Sender -> Alice -> Bob + aliceBobPath := buildMessagePath( + testCase.Generate.Hops[:1], alicePubKey, + ) + senderSessKey := privKeyFromString( + testCase.Generate.Hops[0].PathKeySecret, + ) + aliceBobBlindedPath, err := BuildBlindedPath( + senderSessKey, aliceBobPath, + ) + require.NoError(t, err) + + // Construct the concatenated path. + path := &BlindedPath{ + IntroductionPoint: aliceBobBlindedPath.Path.IntroductionPoint, + BlindingPoint: aliceBobBlindedPath.Path.BlindingPoint, + BlindedHops: append( + aliceBobBlindedPath.Path.BlindedHops, + daveBobBlindedPath.Path.BlindedHops..., + ), + } + + // Check that the constructed path is equal to the test vector path. + require.True(t, equalPubKeys( + testCase.Route.FirstNodeId, path.IntroductionPoint, + )) + require.True(t, equalPubKeys( + testCase.Route.FirstPathKey, path.BlindingPoint, + )) + + for i, hop := range testCase.Route.Hops { + require.True(t, equalPubKeys( + hop.BlindedNodeID, path.BlindedHops[i].BlindedNodePub, + )) + + data, _ := hex.DecodeString(hop.EncryptedRecipientData) + require.Equal(t, data, path.BlindedHops[i].CipherText) + } + + // Assert that each hop is able to decode the encrypted data meant for + // it. + for i, hop := range testCase.Decrypt.Hops { + genData := testCase.Generate.Hops[i] + priv := privKeyFromString(hop.PrivKey) + ephem := pubKeyFromString(genData.EphemeralPubKey) + + // Now we'll decrypt the blinded hop data using the private key + // and the ephemeral public key. + data, err := decryptBlindedHopData( + &PrivKeyECDH{PrivKey: priv}, ephem, + path.BlindedHops[i].CipherText, + ) + require.NoError(t, err) + + // Check if the decrypted data is what we expect it to be. + dataExpected, _ := hex.DecodeString(genData.EncryptedDataTlv) + require.Equal(t, data, dataExpected) + + nextEphem, err := NextEphemeral(&PrivKeyECDH{priv}, ephem) + require.NoError(t, err) + + nextE := privKeyFromString(genData.NextEphemeralPrivKey) + + require.Equal(t, nextE.PubKey(), nextEphem) + } +} + // TestOnionRouteBlinding tests that an onion packet can correctly be processed // by a node in a blinded route. func TestOnionRouteBlinding(t *testing.T) { @@ -190,9 +357,10 @@ func TestOnionRouteBlinding(t *testing.T) { priv := privKeyFromString(hop.NodePrivKey) - if i == introPointIndex { + switch i { + case introPointIndex: blindingPoint = firstBlinding - } else if i == concatIndex { + case concatIndex: blindingPoint = blindingOverride } @@ -208,6 +376,121 @@ func TestOnionRouteBlinding(t *testing.T) { } } +// TestOnionMessageRouteBlinding tests that an onion message packet can +// correctly be processed by a node in a blinded route. +func TestOnionMessageRouteBlinding(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := os.ReadFile(blindedOnionMessageOnionTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // onionMessageJsonTestCase struct defined above. + testCase := &onionMessageJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + + // Extract the original onion message packet to be processed. + onion, err := hex.DecodeString(testCase.OnionMessage.OnionMessagePacket) + require.NoError(t, err) + + onionBytes := bytes.NewReader(onion) + onionPacket := &OnionPacket{} + require.NoError(t, onionPacket.Decode(onionBytes)) + + // peelOnion is a helper closure that can be used to set up a Router + // and use it to process the given onion packet. + peelOnion := func(key *btcec.PrivateKey, + blindingPoint *btcec.PublicKey, + onionPacket *OnionPacket) *ProcessedPacket { + + r := NewRouter(&PrivKeyECDH{PrivKey: key}, NewMemoryReplayLog()) + + require.NoError(t, r.Start()) + defer r.Stop() + + res, err := r.ProcessOnionPacket( + onionPacket, nil, 10, + WithBlindingPoint(blindingPoint), + ) + require.NoError(t, err) + + return res + } + + hops := testCase.Generate.Hops + + // There are some things that the processor of the onion packet will + // only be able to determine from the actual contents of the encrypted + // data it receives. These things include the next_blinding_point for + // the introduction point and the next_blinding_override. The decryption + // of this data is dependent on the encoding chosen by higher layers. + // The test uses TLVs. Since the extraction of this data is dependent + // on layers outside the scope of this library, we provide handle these + // cases manually for the sake of the test. + var ( + firstBlinding = pubKeyFromString(testCase.Route.FirstPathKey) + concatIndex = 1 + blindingOverride = pubKeyFromString( + hops[0].EncodedOnionMessageTLVs.NextPathKeyOverride, + ) + ) + + // Onion message routes are always entirely blinded, so + // the first hop will always use the first blinding + // point. + blindingPoint := firstBlinding + currentOnionPacket := onionPacket + for i, hop := range testCase.Decrypt.Hops { + // We encode the onion message packet to a buffer at each hop to + // compare it to the onion message packet in the test vector. + buff := bytes.NewBuffer(nil) + require.NoError(t, currentOnionPacket.Encode(buff)) + + // hop.OnionMessage contains the onion_message hex string. This + // contains the type 513 (two bytes), the path_key (33 bytes) + // and the length of the onion_message_packet (two bytes). We + // are only interested in the onion_message_packet so we only + // check that part. 2 + 33 + 2 = 37 bytes, so we skip the first + // 37 bytes, which equals 74 hex characters. + const onionMessageHexHeaderLen = 74 + + require.Equal( + t, hop.OnionMessage[onionMessageHexHeaderLen:], + hex.EncodeToString(buff.Bytes()), + ) + + priv := privKeyFromString(hop.PrivKey) + + if i == concatIndex { + blindingPoint = blindingOverride + } + + // With peelOnion we call into ProcessOnionPacket (with the + // functional option WithBlindingPoint) and we expect that the + // onion message packet for this hop is processed without error, + // otherwise peelOnion fails the test. + processedPkt := peelOnion( + priv, blindingPoint, currentOnionPacket, + ) + + // We derive the next blinding point from the current blinding + // point and the private key of the current hop. The new + // blindingPoint will be used to peel the next hop's onion + // unless it is overridden by a blinding override. + blindingPoint, err = NextEphemeral( + &PrivKeyECDH{priv}, blindingPoint, + ) + require.NoError(t, err) + + // We set the current onion packet to the next packet in the + // processed packet. This is the packet that the next hop will + // process. During the next iteration we will run all the above + // checks on this packet. + currentOnionPacket = processedPkt.NextPacket + } +} + type onionBlindingJsonTestCase struct { Generate generateOnionData `json:"generate"` Decrypt decryptData `json:"decrypt"` @@ -223,24 +506,51 @@ type decryptData struct { Hops []decryptHops `json:"hops"` } +type decryptOnionMessageData struct { + Hops []decryptOnionMessageHops `json:"hops"` +} + type decryptHops struct { Onion string `json:"onion"` NodePrivKey string `json:"node_privkey"` NextBlinding string `json:"next_blinding"` } +type decryptOnionMessageHops struct { + OnionMessage string `json:"onion_message"` + PrivKey string `json:"privkey"` + NextNodeID string `json:"next_node_id"` +} + type blindingJsonTestCase struct { Generate generateData `json:"generate"` Route routeData `json:"route"` Unblind unblindData `json:"unblind"` } +type onionMessageJsonTestCase struct { + Generate generateOnionMessageData `json:"generate"` + Route routeOnionMessageData `json:"route"` + OnionMessage onionMessageData `json:"onionmessage"` + Decrypt decryptOnionMessageData `json:"decrypt"` +} + type routeData struct { IntroductionNodeID string `json:"introduction_node_id"` Blinding string `json:"blinding"` Hops []blindedHop `json:"hops"` } +type routeOnionMessageData struct { + FirstNodeId string `json:"first_node_id"` + FirstPathKey string `json:"first_path_key"` + Hops []blindedOnionMessageHop `json:"hops"` +} + +type onionMessageData struct { + OnionMessagePacket string `json:"onion_message_packet"` +} + type unblindData struct { Hops []unblindedHop `json:"hops"` } @@ -249,6 +559,11 @@ type generateData struct { Hops []hopData `json:"hops"` } +type generateOnionMessageData struct { + SessionKey string `json:"session_key"` + Hops []hopOnionMessageData `json:"hops"` +} + type unblindedHop struct { NodePrivKey string `json:"node_privkey"` EphemeralPubKey string `json:"ephemeral_pubkey"` @@ -262,11 +577,31 @@ type hopData struct { EncodedTLVs string `json:"encoded_tlvs"` } +type hopOnionMessageData struct { + PathKeySecret string `json:"path_key_secret"` + EncodedOnionMessageTLVs encodedOnionMessageTLVs `json:"tlvs"` + EncryptedDataTlv string `json:"encrypted_data_tlv"` + EphemeralPubKey string `json:"E"` + NextEphemeralPrivKey string `json:"next_e"` +} + +type encodedOnionMessageTLVs struct { + NextNodeID string `json:"next_node_id"` + NextPathKeyOverride string `json:"next_path_key_override"` + PathKeyOverrideSecret string `json:"path_key_override_secret"` + PathID string `json:"path_id"` +} + type blindedHop struct { BlindedNodeID string `json:"blinded_node_id"` EncryptedData string `json:"encrypted_data"` } +type blindedOnionMessageHop struct { + BlindedNodeID string `json:"blinded_node_id"` + EncryptedRecipientData string `json:"encrypted_recipient_data"` +} + func equalPubKeys(pkStr string, pk *btcec.PublicKey) bool { return hex.EncodeToString(pk.SerializeCompressed()) == pkStr } diff --git a/payload.go b/payload.go index 9e89dad..da5fa30 100644 --- a/payload.go +++ b/payload.go @@ -44,6 +44,10 @@ type HopPayload struct { // HMAC is an HMAC computed over the entire per-hop payload that also // includes the higher-level (optional) associated data bytes. HMAC [HMACSize]byte + + // TLVPayloadGuaranteed is set to true if the payload is guaranteed to + // be a TLVPayload. E.g. in the case of an onion message. + TLVPayloadGuaranteed bool } // NewTLVHopPayload creates a new TLV encoded HopPayload. The payload will be @@ -99,36 +103,13 @@ func (hp *HopPayload) Decode(r io.Reader) error { return err } - var ( - legacyPayload = isLegacyPayloadByte(peekByte[0]) - payloadSize uint16 - ) - - if legacyPayload { - payloadSize = legacyPayloadSize() - hp.Type = PayloadLegacy - } else { - payloadSize, err = tlvPayloadSize(bufReader) - if err != nil { - return err - } - - hp.Type = PayloadTLV + // If the HopPayload is guaranteed to be a TLV payload, we can skip the + // check for the legacy payload byte. + if !hp.TLVPayloadGuaranteed && isLegacyPayloadByte(peekByte[0]) { + return decodeLegacyHopPayload(hp, bufReader) } - // Now that we know the payload size, we'll create a new buffer to - // read it out in full. - // - // TODO(roasbeef): can avoid all these copies - hp.Payload = make([]byte, payloadSize) - if _, err := io.ReadFull(bufReader, hp.Payload[:]); err != nil { - return err - } - if _, err := io.ReadFull(bufReader, hp.HMAC[:]); err != nil { - return err - } - - return nil + return decodeTLVHopPayload(hp, bufReader) } // HopData attempts to extract a set of forwarding instructions from the target @@ -146,6 +127,42 @@ func (hp *HopPayload) HopData() (*HopData, error) { return nil, nil } +// readPayloadAndHMAC reads the payload and HMAC from the reader into the +// HopPayload. +func readPayloadAndHMAC(hp *HopPayload, r io.Reader, payloadSize uint16) error { + // Now that we know the payload size, we'll create a new buffer to read + // it out in full. + hp.Payload = make([]byte, payloadSize) + if _, err := io.ReadFull(r, hp.Payload[:]); err != nil { + return err + } + if _, err := io.ReadFull(r, hp.HMAC[:]); err != nil { + return err + } + + return nil +} + +// decodeTLVHopPayload decodes a TLV hop payload from the passed reader. +func decodeTLVHopPayload(hp *HopPayload, r io.Reader) error { + payloadSize, err := tlvPayloadSize(r) + if err != nil { + return err + } + + hp.Type = PayloadTLV + + return readPayloadAndHMAC(hp, r, payloadSize) +} + +// decodeLegacyHopPayload decodes a legacy hop payload from the passed reader. +func decodeLegacyHopPayload(hp *HopPayload, r io.Reader) error { + payloadSize := legacyPayloadSize() + hp.Type = PayloadLegacy + + return readPayloadAndHMAC(hp, r, payloadSize) +} + // tlvPayloadSize uses the passed reader to extract the payload length encoded // as a var-int. func tlvPayloadSize(r io.Reader) (uint16, error) { @@ -314,8 +331,12 @@ func legacyNumBytes() int { return LegacyHopDataSize } -// isLegacyPayload returns true if the given byte is equal to the 0x00 byte -// which indicates that the payload should be decoded as a legacy payload. +// isLegacyPayloadByte determines if the first byte of a hop payload indicates +// that it is a legacy payload. The first byte of a legacy payload will always +// be 0x00, as this is the realm. For TLV payloads, the first byte is a +// var-int encoding the length of the payload. A TLV stream can be empty, in +// which case its length is 0, which is also encoded as a 0x00 byte. This +// creates an ambiguity between a legacy payload and an empty TLV payload. func isLegacyPayloadByte(b byte) bool { return b == 0x00 } diff --git a/sphinx.go b/sphinx.go index 8e16b23..fa16e65 100644 --- a/sphinx.go +++ b/sphinx.go @@ -41,26 +41,16 @@ const ( LegacyHopDataSize = (RealmByteSize + AddressSize + AmtForwardSize + OutgoingCLTVSize + NumPaddingBytes + HMACSize) - // MaxPayloadSize is the maximum size a payload for a single hop can be. - // This is the worst case scenario of a single hop, consuming all - // available space. We need to know this in order to generate a - // sufficiently long stream of pseudo-random bytes when - // encrypting/decrypting the payload. - MaxPayloadSize = routingInfoSize - - // routingInfoSize is the fixed size of the the routing info. This - // consists of a addressSize byte address and a HMACSize byte HMAC for - // each hop of the route, the first pair in cleartext and the following - // pairs increasingly obfuscated. If not all space is used up, the - // remainder is padded with null-bytes, also obfuscated. - routingInfoSize = 1300 - - // numStreamBytes is the number of bytes produced by our CSPRG for the - // key stream implementing our stream cipher to encrypt/decrypt the mix - // header. The MaxPayloadSize bytes at the end are used to - // encrypt/decrypt the fillers when processing the packet of generating - // the HMACs when creating the packet. - numStreamBytes = routingInfoSize * 2 + // MaxRoutingPayloadSize is the maximum size an `update_add_htlc` + // payload for a single hop can be. This is the worst case scenario of a + // single hop, consuming all available space. We need to know this in + // order to generate a sufficiently long stream of pseudo-random bytes + // when encrypting/decrypting the payload. + MaxRoutingPayloadSize = 1300 + + // MaxOnionMessagePayloadSize is the size of the routing info for a + // onion messaging jumbo onion packet. + MaxOnionMessagePayloadSize = 32768 // keyLen is the length of the keys used to generate cipher streams and // encrypt payloads. Since we use SHA256 to generate the keys, the @@ -72,8 +62,15 @@ const ( ) var ( - ErrMaxRoutingInfoSizeExceeded = fmt.Errorf( - "max routing info size of %v bytes exceeded", routingInfoSize) + ErrStandardRoutingPayloadSizeExceeded = fmt.Errorf( + "max routing payload size of %v bytes exceeded", + MaxRoutingPayloadSize, + ) + + ErrJumboRoutingInfoSizeExceeded = fmt.Errorf( + "max onion message routing payload size of %v bytes exceeded", + MaxOnionMessagePayloadSize, + ) ) // OnionPacket is the onion wrapped hop-to-hop routing information necessary to @@ -102,7 +99,7 @@ type OnionPacket struct { // RoutingInfo is the full routing information for this onion packet. // This encodes all the forwarding instructions for this current hop // and all the hops in the route. - RoutingInfo [routingInfoSize]byte + RoutingInfo []byte // HeaderMAC is an HMAC computed with the shared secret of the routing // data and the associated data for this route. Including the @@ -190,15 +187,29 @@ func generateSharedSecrets(paymentPath []*btcec.PublicKey, return hopSharedSecrets, lastEphemeralPubKey, nil } +// onionPacketCfg is a struct that holds the configuration for creating a new +// onion packet. +type onionPacketCfg struct { + isOnionMessage bool +} + +// OnionPacketOption is a function that can be used to modify the onion packet +// configuration. +type OnionPacketOption func(*onionPacketCfg) + +// WithOnionMessage is a functional option that signals that the onion packet +// being created is an onion message. +func WithOnionMessage() OnionPacketOption { + return func(cfg *onionPacketCfg) { + cfg.isOnionMessage = true + } +} + // NewOnionPacket creates a new onion packet which is capable of obliviously // routing a message through the mix-net path outline by 'paymentPath'. func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, - assocData []byte, pktFiller PacketFiller) (*OnionPacket, error) { - - // Check whether total payload size doesn't exceed the hard maximum. - if paymentPath.TotalPayloadSize() > routingInfoSize { - return nil, ErrMaxRoutingInfoSizeExceeded - } + assocData []byte, pktFiller PacketFiller, + opts ...OnionPacketOption) (*OnionPacket, error) { // If we don't actually have a partially populated route, then we'll // exit early. @@ -207,6 +218,39 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, return nil, fmt.Errorf("route of length zero passed in") } + cfg := &onionPacketCfg{} + for _, o := range opts { + o(cfg) + } + + totalPayloadSize := paymentPath.TotalPayloadSize() + + routingPayloadLen := MaxRoutingPayloadSize + maxRoutingPayloadErr := ErrStandardRoutingPayloadSizeExceeded + if cfg.isOnionMessage && totalPayloadSize > MaxRoutingPayloadSize { + routingPayloadLen = MaxOnionMessagePayloadSize + maxRoutingPayloadErr = ErrJumboRoutingInfoSizeExceeded + } + + // Check whether total payload size doesn't exceed the hard maximum. + if totalPayloadSize > routingPayloadLen { + return nil, maxRoutingPayloadErr + } + + // Before we proceed, we'll check that the payload types of each hop + // in the payment path match the type of onion packet we're creating. + for i := 0; i < numHops; i++ { + hopPayload := (*paymentPath)[i].HopPayload + isLegacy := hopPayload.Type == PayloadLegacy + + // If this is an onion message, we only expect TLV + // payloads. + if cfg.isOnionMessage && isLegacy { + return nil, fmt.Errorf("hop %d has legacy payload, "+ + "but onion messages require TLV", i) + } + } + // We'll force the caller to provide a packet filler, as otherwise we // may default to an insecure filling method (which should only really // be used to generate test vectors). @@ -222,18 +266,20 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, } // Generate the padding, called "filler strings" in the paper. - filler := generateHeaderPadding("rho", paymentPath, hopSharedSecrets) + filler := generateHeaderPadding( + "rho", paymentPath, hopSharedSecrets, routingPayloadLen, + ) // Allocate zero'd out byte slices to store the final mix header packet // and the hmac for each hop. var ( - mixHeader [routingInfoSize]byte + mixHeader = make([]byte, routingPayloadLen) nextHmac [HMACSize]byte hopPayloadBuf bytes.Buffer ) // Fill the packet using the caller specified methodology. - if err := pktFiller(sessionKey, &mixHeader); err != nil { + if err := pktFiller(sessionKey, mixHeader); err != nil { return nil, err } @@ -254,26 +300,26 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, // Next, using the key dedicated for our stream cipher, we'll // generate enough bytes to obfuscate this layer of the onion // packet. - streamBytes := generateCipherStream(rhoKey, routingInfoSize) + streamBytes := generateCipherStream(rhoKey, uint(routingPayloadLen)) payload := paymentPath[i].HopPayload // Before we assemble the packet, we'll shift the current // mix-header to the right in order to make room for this next // per-hop data. shiftSize := payload.NumBytes() - rightShift(mixHeader[:], shiftSize) + rightShift(mixHeader, shiftSize) err := payload.Encode(&hopPayloadBuf) if err != nil { return nil, err } - copy(mixHeader[:], hopPayloadBuf.Bytes()) + copy(mixHeader, hopPayloadBuf.Bytes()) // Once the packet for this hop has been assembled, we'll // re-encrypt the packet by XOR'ing with a stream of bytes // generated using our shared secret. - xor(mixHeader[:], mixHeader[:], streamBytes[:]) + xor(mixHeader, mixHeader, streamBytes) // If this is the "last" hop, then we'll override the tail of // the hop data. @@ -285,7 +331,7 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey, // calculating the MAC, we'll also include the optional // associated data which can allow higher level applications to // prevent replay attacks. - packet := append(mixHeader[:], assocData...) + packet := append(mixHeader, assocData...) nextHmac = calcMac(muKey, packet) hopPayloadBuf.Reset() @@ -322,7 +368,9 @@ func rightShift(slice []byte, num int) { // leaving only the original "filler" bytes produced by this function at the // last hop. Using this methodology, the size of the field stays constant at // each hop. -func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash256) []byte { +func generateHeaderPadding(key string, path *PaymentPath, + sharedSecrets []Hash256, routingInfoLen int) []byte { + numHops := path.TrueRouteLength() // We have to generate a filler that matches all but the last hop (the @@ -332,7 +380,7 @@ func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash25 for i := 0; i < numHops-1; i++ { // Sum up how many frames were used by prior hops. - fillerStart := routingInfoSize + fillerStart := routingInfoLen for _, p := range path[:i] { fillerStart -= p.HopPayload.NumBytes() } @@ -340,10 +388,10 @@ func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash25 // The filler is the part dangling off of the end of the // routingInfo, so offset it from there, and use the current // hop's frame count as its size. - fillerEnd := routingInfoSize + path[i].HopPayload.NumBytes() + fillerEnd := routingInfoLen + path[i].HopPayload.NumBytes() streamKey := generateKey(key, &sharedSecrets[i]) - streamBytes := generateCipherStream(streamKey, numStreamBytes) + streamBytes := generateCipherStream(streamKey, uint(routingInfoLen*2)) xor(filler, filler, streamBytes[fillerStart:fillerEnd]) } @@ -365,7 +413,7 @@ func (f *OnionPacket) Encode(w io.Writer) error { return err } - if _, err := w.Write(f.RoutingInfo[:]); err != nil { + if _, err := w.Write(f.RoutingInfo); err != nil { return err } @@ -404,14 +452,24 @@ func (f *OnionPacket) Decode(r io.Reader) error { return ErrInvalidOnionKey } - if _, err := io.ReadFull(r, f.RoutingInfo[:]); err != nil { + // To figure out the length of the routing info, we'll read all the + // remaining bytes from the reader. + routingInfoAndMAC, err := io.ReadAll(r) + if err != nil { return err } - if _, err := io.ReadFull(r, f.HeaderMAC[:]); err != nil { - return err + // The packet must have at least enough bytes for the HMAC. + if len(routingInfoAndMAC) < HMACSize { + return fmt.Errorf("onion packet is too small, missing HMAC") } + // With the remainder of the packet read, we can now properly slice the + // routing information and the MAC. + routingInfoLen := len(routingInfoAndMAC) - HMACSize + f.RoutingInfo = routingInfoAndMAC[:routingInfoLen] + copy(f.HeaderMAC[:], routingInfoAndMAC[routingInfoLen:]) + return nil } @@ -510,7 +568,8 @@ func (r *Router) Stop() { // processOnionCfg is a set of config values that can be used to modify how an // onion is processed. type processOnionCfg struct { - blindingPoint *btcec.PublicKey + blindingPoint *btcec.PublicKey + isOnionMessage bool } // ProcessOnionOpt defines the signature of a function option that can be used @@ -525,6 +584,14 @@ func WithBlindingPoint(point *btcec.PublicKey) ProcessOnionOpt { } } +// WithIsOnionMessage is a functional option that signals that the onion packet +// being processed is from onion message. +func WithIsOnionMessage() ProcessOnionOpt { + return func(cfg *processOnionCfg) { + cfg.isOnionMessage = true + } +} + // ProcessOnionPacket processes an incoming onion packet which has been forward // to the target Sphinx router. If the encoded ephemeral key isn't on the // target Elliptic Curve, then the packet is rejected. Similarly, if the @@ -545,6 +612,13 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, o(cfg) } + // If this is an onion message, a blinding point must be provided and + // associated data must be nil. + if cfg.isOnionMessage && cfg.blindingPoint == nil && assocData != nil { + return nil, fmt.Errorf("blinding point must be provided for " + + "onion messages, and associated data must be nil") + } + // Compute the shared secret for this onion packet. sharedSecret, err := r.generateSharedSecret( onionPkt.EphemeralKey, cfg.blindingPoint, @@ -560,7 +634,9 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, // Continue to optimistically process this packet, deferring replay // protection until the end to reduce the penalty of multiple IO // operations. - packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) + packet, err := processOnionPacket( + onionPkt, &sharedSecret, assocData, cfg.isOnionMessage, + ) if err != nil { return nil, err } @@ -594,7 +670,9 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte, return nil, err } - return processOnionPacket(onionPkt, &sharedSecret, assocData) + return processOnionPacket( + onionPkt, &sharedSecret, assocData, cfg.isOnionMessage, + ) } // DecryptBlindedHopData uses the router's private key to decrypt data encrypted @@ -625,16 +703,18 @@ func (r *Router) OnionPublicKey() *btcec.PublicKey { // packet. This function returns the next inner onion packet layer, along with // the hop data extracted from the outer onion packet. func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, - assocData []byte) (*OnionPacket, *HopPayload, error) { + assocData []byte, isOnionMessage bool) (*OnionPacket, *HopPayload, + error) { dhKey := onionPkt.EphemeralKey routeInfo := onionPkt.RoutingInfo headerMac := onionPkt.HeaderMAC + routingInfoLen := len(routeInfo) // Using the derived shared secret, ensure the integrity of the routing // information by checking the attached MAC without leaking timing // information. - message := append(routeInfo[:], assocData...) + message := append(routeInfo, assocData...) calculatedMac := calcMac(generateKey("mu", sharedSecret), message) if !hmac.Equal(headerMac[:], calculatedMac[:]) { return nil, nil, ErrInvalidOnionHMAC @@ -644,13 +724,13 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // layer off the routing info revealing the routing information for the // next hop. streamBytes := generateCipherStream( - generateKey("rho", sharedSecret), numStreamBytes, + generateKey("rho", sharedSecret), uint(routingInfoLen*2), ) - zeroBytes := bytes.Repeat([]byte{0}, MaxPayloadSize) - headerWithPadding := append(routeInfo[:], zeroBytes...) + zeroBytes := bytes.Repeat([]byte{0}, routingInfoLen) + headerWithPadding := append(routeInfo, zeroBytes...) - var hopInfo [numStreamBytes]byte - xor(hopInfo[:], headerWithPadding, streamBytes) + hopInfo := make([]byte, routingInfoLen*2) + xor(hopInfo, headerWithPadding, streamBytes) // Randomize the DH group element for the next hop using the // deterministic blinding factor. @@ -660,15 +740,16 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // With the MAC checked, and the payload decrypted, we can now parse // out the payload so we can derive the specified forwarding // instructions. - var hopPayload HopPayload - if err := hopPayload.Decode(bytes.NewReader(hopInfo[:])); err != nil { + hopPayload := HopPayload{TLVPayloadGuaranteed: isOnionMessage} + err := hopPayload.Decode(bytes.NewReader(hopInfo)) + if err != nil { return nil, nil, err } // With the necessary items extracted, we'll copy of the onion packet // for the next node, snipping off our per-hop data. - var nextMixHeader [routingInfoSize]byte - copy(nextMixHeader[:], hopInfo[hopPayload.NumBytes():]) + var nextMixHeader = make([]byte, routingInfoLen) + copy(nextMixHeader, hopInfo[hopPayload.NumBytes():]) innerPkt := &OnionPacket{ Version: onionPkt.Version, EphemeralKey: nextDHKey, @@ -683,7 +764,7 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // packets. The processed packets returned from this method should only be used // if the packet was not flagged as a replayed packet. func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256, - assocData []byte) (*ProcessedPacket, error) { + assocData []byte, isOnionMessage bool) (*ProcessedPacket, error) { // First, we'll unwrap an initial layer of the onion packet. Typically, // we'll only have a single layer to unwrap, However, if the sender has @@ -693,7 +774,7 @@ func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // they can properly check the HMAC and unwrap a layer for their // handoff hop. innerPkt, outerHopPayload, err := unwrapPacket( - onionPkt, sharedSecret, assocData, + onionPkt, sharedSecret, assocData, isOnionMessage, ) if err != nil { return nil, err @@ -703,7 +784,7 @@ func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // However if the uncovered 'nextMac' is all zeroes, then this // indicates that we're the final hop in the route. var action ProcessCode = MoreHops - if bytes.Compare(zeroHMAC[:], outerHopPayload.HMAC[:]) == 0 { + if bytes.Equal(zeroHMAC[:], outerHopPayload.HMAC[:]) { action = ExitNode } @@ -794,7 +875,9 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, // Continue to optimistically process this packet, deferring replay // protection until the end to reduce the penalty of multiple IO // operations. - packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) + packet, err := processOnionPacket( + onionPkt, &sharedSecret, assocData, cfg.isOnionMessage, + ) if err != nil { return err } diff --git a/sphinx_test.go b/sphinx_test.go index 485ac5d..f073b3e 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -2,6 +2,7 @@ package sphinx import ( "bytes" + "crypto/rand" "encoding/hex" "encoding/json" "fmt" @@ -12,6 +13,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) @@ -104,10 +106,187 @@ func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacke return nodes, &route, &hopsData, fwdMsg, nil } +func newOnionMessageRoute(numHops int) (*OnionPacket, *PaymentPath, []*Router, + error) { + + if numHops < 2 { + return nil, nil, nil, fmt.Errorf("at least 2 hops are " + + "required to create an onion message route") + } + + // Create routers for each hop. + nodes := make([]*Router, numHops) + for i := 0; i < numHops; i++ { + privKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to generate "+ + "random key for sphinx node: %v", err) + } + nodes[i] = NewRouter( + &PrivKeyECDH{PrivKey: privKey}, NewMemoryReplayLog(), + ) + } + + // Split the nodes into two parts for creating two blinded paths. + mid := numHops / 2 + firstPathNodes := nodes[:mid] + secondPathNodes := nodes[mid:] + + // Create the sessions keys for the two blinded paths. + firstSessionKey, _ := btcec.NewPrivateKey() + secondSessionKey, _ := btcec.NewPrivateKey() + + // Create the first blinded path, adding a next_path_key_override TLV + // at the last node. + firstPathInfos := make([]*HopInfo, len(firstPathNodes)) + for i, node := range firstPathNodes { + nextNodeID := node.onionKey.PubKey().SerializeCompressed() + var b bytes.Buffer + var tlvStream *tlv.Stream + var err error + if i == len(firstPathNodes)-1 { + secondsSessPub := secondSessionKey.PubKey() + pathKeyOverride := secondsSessPub.SerializeCompressed() + tlvStream, err = tlv.NewStream( + tlv.MakePrimitiveRecord(4, &nextNodeID), + tlv.MakePrimitiveRecord(8, &pathKeyOverride), + ) + } else { + tlvStream, err = tlv.NewStream( + tlv.MakePrimitiveRecord(4, &nextNodeID), + ) + } + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to create "+ + "TLV stream: %v", err) + } + if err := tlvStream.Encode(&b); err != nil { + return nil, nil, nil, fmt.Errorf("unable to encode "+ + "TLV stream: %v", err) + } + firstPathInfos[i] = &HopInfo{ + NodePub: node.onionKey.PubKey(), + PlainText: b.Bytes(), + } + } + firstBlindedPath, err := BuildBlindedPath( + firstSessionKey, firstPathInfos, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("error generating first "+ + "blinded path: %v", err) + } + + // Create the second blinded path, omitting the next_node_id TLV for the + // last node. + secondPathInfos := make([]*HopInfo, len(secondPathNodes)) + for i, node := range secondPathNodes { + nextNodeID := node.onionKey.PubKey().SerializeCompressed() + var tlvStream *tlv.Stream + var err error + if i == len(secondPathNodes)-1 { + pathID := make([]byte, 20) + if _, err := rand.Read(pathID); err != nil { + return nil, nil, nil, fmt.Errorf("unable to "+ + "generate random path ID: %v", err) + } + tlvStream, err = tlv.NewStream( + tlv.MakePrimitiveRecord(6, &pathID), + ) + } else { + tlvStream, err = tlv.NewStream( + tlv.MakePrimitiveRecord(4, &nextNodeID), + ) + } + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to create "+ + "TLV stream: %v", err) + } + var b bytes.Buffer + if err := tlvStream.Encode(&b); err != nil { + return nil, nil, nil, fmt.Errorf("unable to encode "+ + "TLV stream: %v", err) + } + + secondPathInfos[i] = &HopInfo{ + NodePub: node.onionKey.PubKey(), + PlainText: b.Bytes(), + } + } + secondBlindedPath, err := BuildBlindedPath( + secondSessionKey, secondPathInfos, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("error generating second "+ + "blinded path: %v", err) + } + + blindedPath := &BlindedPath{ + IntroductionPoint: firstBlindedPath.Path.IntroductionPoint, + BlindingPoint: firstBlindedPath.Path.BlindingPoint, + BlindedHops: append( + firstBlindedPath.Path.BlindedHops, + secondBlindedPath.Path.BlindedHops..., + ), + } + + // Create the route from the blinded path, always adding the + // hop.CipherText as a TLV field type 4. + var route PaymentPath + for i, hop := range blindedPath.BlindedHops { + var payload []byte + var b bytes.Buffer + var tlvStream *tlv.Stream + var err error + + if i == len(blindedPath.BlindedHops)-1 { + hello := []byte("hello") + tlvStream, err = tlv.NewStream( + tlv.MakePrimitiveRecord(4, &hop.CipherText), + tlv.MakePrimitiveRecord(65, &hello), + ) + } else { + tlvStream, err = tlv.NewStream( + tlv.MakePrimitiveRecord(4, &hop.CipherText), + ) + } + + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to create "+ + "TLV stream: %v", err) + } + + if err := tlvStream.Encode(&b); err != nil { + return nil, nil, nil, fmt.Errorf("unable to encode "+ + "TLV stream: %v", err) + } + payload = b.Bytes() + route[i] = OnionHop{ + NodePub: *hop.BlindedNodePub, + HopPayload: HopPayload{ + Type: PayloadTLV, + Payload: payload, + }, + } + } + + // Generate the onion packet. + sessionKey, _ := btcec.NewPrivateKey() + onionPacket, err := NewOnionPacket( + &route, sessionKey, nil, DeterministicPacketFiller, + WithOnionMessage(), + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to create onion "+ + "packet: %v", err) + } + + return onionPacket, &route, nodes, nil +} + func TestBolt4Packet(t *testing.T) { var ( - route PaymentPath - hopsData []HopData + route PaymentPath ) for i, pubKeyHex := range bolt4PubKeys { pubKeyBytes, err := hex.DecodeString(pubKeyHex) @@ -125,7 +304,6 @@ func TestBolt4Packet(t *testing.T) { OutgoingCltv: uint32(i), } copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8)) - hopsData = append(hopsData, hopData) hopPayload, err := NewLegacyHopPayload(&hopData) if err != nil { @@ -157,13 +335,160 @@ func TestBolt4Packet(t *testing.T) { t.Fatalf("unable to decode onion packet: %v", err) } - if bytes.Compare(b.Bytes(), finalPacket) != 0 { + if !bytes.Equal(b.Bytes(), finalPacket) { t.Fatalf("final packet does not match expected BOLT 4 packet, "+ "want: %s, got %s", hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) } } +// TestTLVPayloadMessagePacket tests the creation and encoding of an onion +// message packet that uses a TLV payload for each hop in the route. This test +// uses the test vectors defined in the BOLT 4 specification. The test reads a +// JSON file containing a predefined route, session key, and the expected final +// onion packet. It then constructs the route hop-by-hop, manually creating the +// TLV payload for each, before creating a new onion packet with NewOnionPacket. +// The test concludes by asserting that the newly encoded packet is identical to +// the one specified in the test vector. +func TestTLVPayloadMessagePacket(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw JSON file at the target location. + jsonBytes, err := os.ReadFile(testOnionMessageFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our jsonTestCase + // struct defined above. + testCase := &onionMessageJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + + // Next, we'll populate a new OnionHop using the information included + // in this test case. + var route PaymentPath + for i, hop := range testCase.Route.Hops { + pubKeyBytes, err := hex.DecodeString(hop.BlindedNodeID) + require.NoError(t, err) + + pubKey, err := btcec.ParsePubKey(pubKeyBytes) + require.NoError(t, err) + + encryptedRecipientData, err := hex.DecodeString( + hop.EncryptedRecipientData, + ) + require.NoError(t, err) + + // Manually encode our onion payload + records := []tlv.Record{} + + if i == len(testCase.Route.Hops)-1 { + helloBytes := []byte("hello") + records = append(records, tlv.MakePrimitiveRecord( + 1, &helloBytes, + )) + } + + records = append(records, tlv.MakePrimitiveRecord( + 4, &encryptedRecipientData, + )) + + stream, err := tlv.NewStream(records...) + require.NoError(t, err, "new stream") + + b := new(bytes.Buffer) + require.NoError(t, stream.Encode(b), "encode") + + route[i] = OnionHop{ + NodePub: *pubKey, + HopPayload: HopPayload{ + Type: PayloadTLV, + Payload: b.Bytes(), + }, + } + } + + finalPacket, err := hex.DecodeString( + testCase.OnionMessage.OnionMessagePacket, + ) + require.NoError(t, err) + + sessionKeyBytes, err := hex.DecodeString(testCase.Generate.SessionKey) + + require.NoError(t, err) + + // With all the required data assembled, we'll craft a new packet. + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) + + pkt, err := NewOnionPacket( + &route, sessionKey, nil, DeterministicPacketFiller, + WithOnionMessage(), + ) + require.NoError(t, err) + + var b bytes.Buffer + require.NoError(t, pkt.Encode(&b)) + + // Finally, we expect that our packet matches the packet included in + // the spec's test vectors. + require.Equalf(t, finalPacket, b.Bytes(), "final packet does not "+ + "match expected BOLT 4 packet, want: %s, got %s", + hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes())) +} + +// TestProcessOnionMessageZeroLengthPayload tests that we can properly process an +// onion message that has a zero-length payload. +func TestProcessOnionMessageZeroLengthPayload(t *testing.T) { + t.Parallel() + + // First, create a router that will be the destination of the onion + // message. + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + router := NewRouter(&PrivKeyECDH{privKey}, NewMemoryReplayLog()) + err = router.Start() + require.NoError(t, err) + defer router.Stop() + + // Next, create a session key for the onion packet. + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // We'll create a simple one-hop path. + path := &PaymentPath{ + { + NodePub: *privKey.PubKey(), + }, + } + + // The hop payload will be an empty TLV payload. + payload, err := NewTLVHopPayload(nil) + require.NoError(t, err) + path[0].HopPayload = payload + + // Now, create the onion packet. + onionPacket, err := NewOnionPacket( + path, sessionKey, nil, DeterministicPacketFiller, + WithOnionMessage(), + ) + require.NoError(t, err) + + // We'll now process the packet, making sure to indicate that this is + // an onion message. + processedPacket, err := router.ProcessOnionPacket( + onionPacket, nil, 0, WithIsOnionMessage(), + ) + require.NoError(t, err) + + // The packet should be decoded as an exit node. + require.EqualValues(t, ExitNode, processedPacket.Action) + + // The payload should be of type TLV. + require.Equal(t, PayloadTLV, processedPacket.Payload.Type) + + // And the payload should be empty. + require.Empty(t, processedPacket.Payload.Payload) +} + func TestSphinxCorrectness(t *testing.T) { nodes, _, hopDatas, fwdMsg, err := newTestRoute(testLegacyRouteNumHops) if err != nil { @@ -531,6 +856,18 @@ func mustNewLegacyHopPayload(hopData *HopData) HopPayload { return payload } +// TestPaymentPathTotalPayloadSizeExceeds1300 tests that a PaymentPath can have +// a TotalPayloadSize greater than 1300 bytes. +func TestPaymentPathTotalPayloadSizeExceeds1300(t *testing.T) { + _, route, _, err := newOnionMessageRoute(15) + require.NoError(t, err, "newOnionMessageRoute should not return an "+ + "error") + + totalSize := route.TotalPayloadSize() + require.Greater(t, totalSize, 1300, "TotalPayloadSize should be "+ + "greater than 1300") +} + // TestSphinxHopVariableSizedPayloads tests that we're able to fully decode an // EOB payload that was targeted at the final hop in a route, and also when // intermediate nodes have EOB data encoded as well. Additionally, we test that @@ -650,7 +987,7 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { Payload: bytes.Repeat([]byte("a"), 500), }, }, - expectedError: ErrMaxRoutingInfoSizeExceeded, + expectedError: ErrStandardRoutingPayloadSizeExceeded, }, } @@ -757,6 +1094,9 @@ const ( // testTLVFileName is the name of the tlv-payload-only onion test file. testTLVFileName = "testdata/onion-test.json" + + // testOnionMessageFileName is the name of the onion message test file. + testOnionMessageFileName = "testdata/blinded-onion-message-onion-test.json" ) type jsonHop struct { diff --git a/testdata/blinded-onion-message-onion-test.json b/testdata/blinded-onion-message-onion-test.json new file mode 100644 index 0000000..fe5191e --- /dev/null +++ b/testdata/blinded-onion-message-onion-test.json @@ -0,0 +1,143 @@ +{ + "comment": "Test vector creating an onionmessage, including joining an existing one", + "generate": { + "comment": "This sections contains test data for Dave's blinded path Bob->Dave; sender has to prepend a hop to Alice to reach Bob", + "session_key": "0303030303030303030303030303030303030303030303030303030303030303", + "hops": [ + { + "alias": "Alice", + "comment": "Alice->Bob: note next_path_key_override to match that give by Dave for Bob", + "path_key_secret": "6363636363636363636363636363636363636363636363636363636363636363", + "tlvs": { + "next_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "next_path_key_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "path_key_override_secret": "0101010101010101010101010101010101010101010101010101010101010101" + }, + "encrypted_data_tlv": "04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c0821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "ss": "c04d2a4c518241cb49f2800eea92554cb543f268b4c73f85693541e86d649205", + "HMAC256('blinded_node_id', ss)": "bc5388417c8db33af18ab7ba43f6a5641861f7b0ecb380e501a739af446a7bf4", + "blinded_node_id": "02d1c3d73f8cac67e7c5b6ec517282d5ba0a52b06a29ec92ff01e12decf76003c1", + "E": "031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99", + "H(E || ss)": "83377bd6096f82df3a46afec20d68f3f506168f2007f6e86c2dc267417de9e34", + "next_e": "bf3e8999518c0bb6e876abb0ae01d44b9ba211720048099a2ba5a83afd730cad01", + "rho": "6926df9d4522b26ad4330a51e3481208e4816edd9ae4feaf311ea0342eb90c44", + "encrypted_recipient_data": "49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b" + }, + { + "alias": "Bob", + "comment": "Bob->Carol", + "path_key_secret": "0101010101010101010101010101010101010101010101010101010101010101", + "tlvs": { + "next_node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "unknown_tag_561": "123456" + }, + "encrypted_data_tlv": "0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007fd023103123456", + "ss": "196f1f3e0be9d65f88463c1ab63e07f41b4e7c0368c28c3e6aa290cc0d22eaed", + "HMAC256('blinded_node_id', ss)": "c331d35827bdd509a02f1e64d48c7f0d7b2603355abbb1a3733c86e50135608e", + "blinded_node_id": "03f1465ca5cf3ec83f16f9343d02e6c24b76993a93e1dea2398f3147a9be893d7a", + "E": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "H(E || ss)": "1889a6cf337d9b34f80bb23a91a2ca194e80d7614f0728bdbda153da85e46b69", + "next_e": "f7ab6dca6152f7b6b0c9d7c82d716af063d72d8eef8816dfc51a8ae828fa7dce01", + "rho": "db991242ce366ab44272f38383476669b713513818397a00d4808d41ea979827", + "encrypted_recipient_data": "adf6771d3983b7f543d1b3d7a12b440b2bd3e1b3b8d6ec1023f6dec4f0e7548a6f57f6dbe9573b0a0f24f7c5773a7dd7a7bdb6bd0ee686d759f5" + }, + { + "alias": "Carol", + "comment": "Carol->Dave", + "path_key_secret": "f7ab6dca6152f7b6b0c9d7c82d716af063d72d8eef8816dfc51a8ae828fa7dce", + "tlvs": { + "padding": "0000000000", + "next_node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" + }, + "encrypted_data_tlv": "010500000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "ss": "c7b33d74a723e26331a91c15ae5bc77db28a18b801b6bc5cd5bba98418303a9d", + "HMAC256('blinded_node_id', ss)": "a684c7495444a8cc2a6dfdecdf0819f3cdf4e86b81cc14e39825a40872ecefff", + "blinded_node_id": "035dbc0493aa4e7eea369d6a06e8013fd03e66a5eea91c455ed65950c4942b624b", + "E": "02b684babfd400c8dd48b367e9754b8021a3594a34dc94d7101776c7f6a86d0582", + "H(E || ss)": "2d80c5619a5a68d22dd3d784cab584c2718874922735d36cb36a179c10a796ca", + "next_e": "5de52bb427cc148bf23e509fdc18012004202517e80abcfde21612ae408e6cea01", + "rho": "739851e89b61cab34ee9ba7d5f3c342e4adc8b91a72991664026f68a685f0bdc", + "encrypted_recipient_data": "d8903df7a79ac799a0b59f4ba22f6a599fa32e7ff1a8325fc22b88d278ce3e4840af02adfb82d6145a189ba50c2219c9e4351e634d198e0849ac" + }, + { + "alias": "Dave", + "comment": "Dave is final node, hence path_id", + "path_key_secret": "5de52bb427cc148bf23e509fdc18012004202517e80abcfde21612ae408e6cea", + "tlvs": { + "padding": "", + "path_id": "deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0", + "unknown_tag_65535": "06c1" + }, + "encrypted_data_tlv": "01000620deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0fdffff0206c1", + "ss": "024955ed0d4ebbfab13498f5d7aacd00bf096c8d9ed0473cdfc96d90053c86b7", + "HMAC256('blinded_node_id', ss)": "3f5612df60f050ac571aeaaf76655e138529bea6d23293ebe15659f2588cd039", + "blinded_node_id": "0237bf019fa0fbecde8b4a1c7b197c9c1c76f9a23d67dd55bb5e42e1f50bb771a6", + "E": "025aaca62db7ce6b46386206ef9930daa32e979a35cb185a41cb951aa7d254b03c", + "H(E || ss)": "db5719e79919d706eab17eebaad64bd691e56476a42f0e26ae60caa9082f56fa", + "next_e": "ae31d2fbbf2f59038542c13287b9b624ea1a212c82be87c137c3d92aa30a185d01", + "rho": "c47cde57edc790df7b9b6bf921aff5e5eee43f738ab8fa9103ef675495f3f50e", + "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" + } + ] + }, + "route": { + "comment": "The resulting blinded route Alice to Dave.", + "first_node_id": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "first_path_key": "031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99", + "hops": [ + { + "blinded_node_id": "02d1c3d73f8cac67e7c5b6ec517282d5ba0a52b06a29ec92ff01e12decf76003c1", + "encrypted_recipient_data": "49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b" + }, + { + "blinded_node_id": "03f1465ca5cf3ec83f16f9343d02e6c24b76993a93e1dea2398f3147a9be893d7a", + "encrypted_recipient_data": "adf6771d3983b7f543d1b3d7a12b440b2bd3e1b3b8d6ec1023f6dec4f0e7548a6f57f6dbe9573b0a0f24f7c5773a7dd7a7bdb6bd0ee686d759f5" + }, + { + "blinded_node_id": "035dbc0493aa4e7eea369d6a06e8013fd03e66a5eea91c455ed65950c4942b624b", + "encrypted_recipient_data": "d8903df7a79ac799a0b59f4ba22f6a599fa32e7ff1a8325fc22b88d278ce3e4840af02adfb82d6145a189ba50c2219c9e4351e634d198e0849ac" + }, + { + "blinded_node_id": "0237bf019fa0fbecde8b4a1c7b197c9c1c76f9a23d67dd55bb5e42e1f50bb771a6", + "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" + } + ] + }, + "onionmessage": { + "comment": "An onion message which sends a 'hello' to Dave", + "unknown_tag_1": "68656c6c6f", + "onion_message_packet": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33793b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad7728e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb" + }, + "decrypt": { + "comment": "This section contains the internal values generated by intermediate nodes when decrypting the onion.", + "hops": [ + { + "alias": "Alice", + "privkey": "4141414141414141414141414141414141414141414141414141414141414141", + "onion_message": "0201031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd9905560002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33793b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad7728e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb", + "next_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c" + }, + { + "alias": "Bob", + "privkey": "4242424242424242424242424242424242424242424242424242424242424242", + "onion_message": "0201031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f05560002536d53f93796cad550b6c68662dca41f7e8c221c31022c64dd1a627b2df3982b25eac261e88369cfc66e1e3b6d9829cb3dcd707046e68a7796065202a7904811bf2608c5611cf74c9eb5371c7eb1a4428bb39a041493e2a568ddb0b2482a6cc6711bc6116cef144ebf988073cb18d9dd4ce2d3aa9de91a7dc6d7c6f11a852024626e66b41ba1158055505dff9cb15aa51099f315564d9ee3ed6349665dc3e209eedf9b5805ee4f69d315df44c80e63d0e2efbdab60ec96f44a3447c6a6ddb1efb6aa4e072bde1dab974081646bfddf3b02daa2b83847d74dd336465e76e9b8fecc2b0414045eeedfc39939088a76820177dd1103c99939e659beb07197bab9f714b30ba8dc83738e9a6553a57888aaeda156c68933a2f4ff35e3f81135076b944ed9856acbfee9c61299a5d1763eadd14bf5eaf71304c8e165e590d7ecbcd25f1650bf5b6c2ad1823b2dc9145e168974ecf6a2273c94decff76d94bc6708007a17f22262d63033c184d0166c14f41b225a956271947aae6ce65890ed8f0d09c6ffe05ec02ee8b9de69d7077a0c5adeb813aabcc1ba8975b73ab06ddea5f4db3c23a1de831602de2b83f990d4133871a1a81e53f86393e6a7c3a7b73f0c099fa72afe26c3027bb9412338a19303bd6e6591c04fb4cde9b832b5f41ae199301ea8c303b5cef3aca599454273565de40e1148156d1f97c1aa9e58459ab318304075e034f5b7899c12587b86776a18a1da96b7bcdc22864fccc4c41538ebce92a6f054d53bf46770273a70e75fe0155cd6d2f2e937465b0825ce3123b8c206fac4c30478fa0f08a97ade7216dce11626401374993213636e93545a31f500562130f2feb04089661ad8c34d5a4cbd2e4e426f37cb094c786198a220a2646ecadc38c04c29ee67b19d662c209a7b30bfecc7fe8bf7d274de0605ee5df4db490f6d32234f6af639d3fce38a2801bcf8d51e9c090a6c6932355a83848129a378095b34e71cb8f51152dc035a4fe8e802fec8de221a02ba5afd6765ce570bef912f87357936ea0b90cb2990f56035e89539ec66e8dbd6ed50835158614096990e019c3eba3d7dd6a77147641c6145e8b17552cd5cf7cd163dd40b9eaeba8c78e03a2cd8c0b7997d6f56d35f38983a202b4eb8a54e14945c4de1a6dde46167e11708b7a5ff5cb9c0f7fc12fae49a012aa90bb1995c038130b749c48e6f1ffb732e92086def42af10fbc460d94abeb7b2fa744a5e9a491d62a08452be8cf2fdef573deedc1fe97098bce889f98200b26f9bb99da9aceddda6d793d8e0e44a2601ef4590cfbb5c3d0197aac691e3d31c20fd8e38764962ca34dabeb85df28feabaf6255d4d0df3d814455186a84423182caa87f9673df770432ad8fdfe78d4888632d460d36d2719e8fa8e4b4ca10d817c5d6bc44a8b2affab8c2ba53b8bf4994d63286c2fad6be04c28661162fa1a67065ecda8ba8c13aee4a8039f4f0110e0c0da2366f178d8903e19136dad6df9d8693ce71f3a270f9941de2a93d9b67bc516207ac1687bf6e00b29723c42c7d9c90df9d5e599dbeb7b73add0a6a2b7aba82f98ac93cb6e60494040445229f983a81c34f7f686d166dfc98ec23a6318d4a02a311ac28d655ea4e0f9c3014984f31e621ef003e98c373561d9040893feece2e0fa6cd2dd565e6fbb2773a2407cb2c3273c306cf71f427f2e551c4092e067cf9869f31ac7c6c80dd52d4f85be57a891a41e34be0d564e39b4af6f46b85339254a58b205fb7e10e7d0470ee73622493f28c08962118c23a1198467e72c4ae1cd482144b419247a5895975ea90d135e2a46ef7e5794a1551a447ff0a0d299b66a7f565cd86531f5e7af5408d85d877ce95b1df12b88b7d5954903a5296325ba478ba1e1a9d1f30a2d5052b2e2889bbd64f72c72bc71d8817288a2", + "next_node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007" + }, + { + "alias": "Carol", + "privkey": "4343434343434343434343434343434343434343434343434343434343434343", + "onion_message": "020102b684babfd400c8dd48b367e9754b8021a3594a34dc94d7101776c7f6a86d0582055600029a77e8523162efa1f4208f4f2050cd5c386ddb6ce6d36235ea569d217ec52209fb85fdf7dbc4786c373eebdba0ddc184cfbe6da624f610e93f62c70f2c56be1090b926359969f040f932c03f53974db5656233bd60af375517d4323002937d784c2c88a564bcefe5c33d3fc21c26d94dfacab85e2e19685fd2ff4c543650958524439b6da68779459aee5ffc9dc543339acec73ff43be4c44ddcbe1c11d50e2411a67056ba9db7939d780f5a86123fdd3abd6f075f7a1d78ab7daf3a82798b7ec1e9f1345bc0d1e935098497067e2ae5a51ece396fcb3bb30871ad73aee51b2418b39f00c8e8e22be4a24f4b624e09cb0414dd46239de31c7be035f71e8da4f5a94d15b44061f46414d3f355069b5c5b874ba56704eb126148a22ec873407fe118972127e63ff80e682e410f297f23841777cec0517e933eaf49d7e34bd203266b42081b3a5193b51ccd34b41342bc67cf73523b741f5c012ba2572e9dda15fbe131a6ac2ff24dc2a7622d58b9f3553092cfae7fae3c8864d95f97aa49ec8edeff5d9f5782471160ee412d82ff6767030fc63eec6a93219a108cd41433834b26676a39846a944998796c79cd1cc460531b8ded659cedfd8aecefd91944f00476f1496daafb4ea6af3feacac1390ea510709783c2aa81a29de27f8959f6284f4684102b17815667cbb0645396ac7d542b878d90c42a1f7f00c4c4eedb2a22a219f38afadb4f1f562b6e000a94e75cc38f535b43a3c0384ccef127fde254a9033a317701c710b2b881065723486e3f4d3eea5e12f374a41565fe43fa137c1a252c2153dde055bb343344c65ad0529010ece29bbd405effbebfe3ba21382b94a60ac1a5ffa03f521792a67b30773cb42e862a8a02a8bbd41b842e115969c87d1ff1f8c7b5726b9f20772dd57fe6e4ea41f959a2a673ffad8e2f2a472c4c8564f3a5a47568dd75294b1c7180c500f7392a7da231b1fe9e525ea2d7251afe9ca52a17fe54a116cb57baca4f55b9b6de915924d644cba9dade4ccc01939d7935749c008bafc6d3ad01cd72341ce5ddf7a5d7d21cf0465ab7a3233433aef21f9acf2bfcdc5a8cc003adc4d82ac9d72b36eb74e05c9aa6ccf439ac92e6b84a3191f0764dd2a2e0b4cc3baa08782b232ad6ecd3ca6029bc08cc094aef3aebddcaddc30070cb6023a689641de86cfc6341c8817215a4650f844cd2ca60f2f10c6e44cfc5f23912684d4457bf4f599879d30b79bf12ef1ab8d34dddc15672b82e56169d4c770f0a2a7a960b1e8790773f5ff7fce92219808f16d061cc85e053971213676d28fb48925e9232b66533dbd938458eb2cc8358159df7a2a2e4cf87500ede2afb8ce963a845b98978edf26a6948d4932a6b95d022004556d25515fe158092ce9a913b4b4a493281393ca731e8d8e5a3449b9d888fc4e73ffcbb9c6d6d66e88e03cf6e81a0496ede6e4e4172b08c000601993af38f80c7f68c9d5fff9e0e215cff088285bf039ca731744efcb7825a272ca724517736b4890f47e306b200aa2543c363e2c9090bcf3cf56b5b86868a62471c7123a41740392fc1d5ab28da18dca66618e9af7b42b62b23aba907779e73ca03ec60e6ab9e0484b9cae6578e0fddb6386cb3468506bf6420298bf4a690947ab582255551d82487f271101c72e19e54872ab47eae144db66bc2f8194a666a5daec08d12822cb83a61946234f2dfdbd6ca7d8763e6818adee7b401fcdb1ac42f9df1ac5cc5ac131f2869013c8d6cd29d4c4e3d05bccd34ca83366d616296acf854fa05149bfd763a25b9938e96826a037fdcb85545439c76df6beed3bdbd01458f9cf984997cc4f0a7ac3cc3f5e1eeb59c09cadcf5a537f16e444149c8f17d4bdaef16c9fbabc5ef06eb0f0bf3a07a1beddfeacdaf1df5582d6dbd6bb808d6ab31bc22e5d7", + "next_node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" + }, + { + "alias": "Dave", + "privkey": "4444444444444444444444444444444444444444444444444444444444444444", + "onion_message": "0201025aaca62db7ce6b46386206ef9930daa32e979a35cb185a41cb951aa7d254b03c055600025550b2910294fa73bda99b9de9c851be9cbb481e23194a1743033630efba546b86e7d838d0f6e9cc0ed088dbf6889f0dceca3bfc745bd77d013a31311fa932a8bf1d28387d9ff521eabc651dee8f861fed609a68551145a451f017ec44978addeee97a423c08445531da488fd1ddc998e9cdbfcea59517b53fbf1833f0bbe6188dba6ca773a247220ec934010daca9cc185e1ceb136803469baac799e27a0d82abe53dc48a06a55d1f643885cc7894677dd20a4e4152577d1ba74b870b9279f065f9b340cedb3ca13b7df218e853e10ccd1b59c42a2acf93f489e170ee4373d30ab158b60fc20d3ba73a1f8c750951d69fb5b9321b968ddc8114936412346aff802df65516e1c09c51ef19849ff36c0199fd88c8bec301a30fef0c7cb497901c038611303f64e4174b5daf42832aa5586b84d2c9b95f382f4269a5d1bd4be898618dc78dfd451170f72ca16decac5b03e60702112e439cadd104fb3bbb3d5023c9b80823fdcd0a212a7e1aaa6eeb027adc7f8b3723031d135a09a979a4802788bb7861c6cc85501fb91137768b70aeab309b27b885686604ffc387004ac4f8c44b101c39bc0597ef7fd957f53fc5051f534b10eb3852100962b5e58254e5558689913c26ad6072ea41f5c5db10077cfc91101d4ae393be274c74297da5cc381cd88d54753aaa7df74b2f9da8d88a72bc9218fcd1f19e4ff4aace182312b9509c5175b6988f044c5756d232af02a451a02ca752f3c52747773acff6fd07d2032e6ce562a2c42105d106eba02d0b1904182cdc8c74875b082d4989d3a7e9f0e73de7c75d357f4af976c28c0b206c5e8123fc2391d078592d0d5ff686fd245c0a2de2e535b7cca99c0a37d432a8657393a9e3ca53eec1692159046ba52cb9bc97107349d8673f74cbc97e231f1108005c8d03e24ca813cea2294b39a7a493bcc062708f1f6cf0074e387e7d50e0666ce784ef4d31cb860f6cad767438d9ea5156ff0ae86e029e0247bf94df75ee0cda4f2006061455cb2eaff513d558863ae334cef7a3d45f55e7cc13153c6719e9901c1d4db6c03f643b69ea4860690305651794284d9e61eb848ccdf5a77794d376f0af62e46d4835acce6fd9eef5df73ebb8ea3bb48629766967f446e744ecc57ff3642c4aa1ccee9a2f72d5caa75fa05787d08b79408fce792485fdecdc25df34820fb061275d70b84ece540b0fc47b2453612be34f2b78133a64e812598fbe225fd85415f8ffe5340ce955b5fd9d67dd88c1c531dde298ed25f96df271558c812c26fa386966c76f03a6ebccbca49ac955916929bd42e134f982dde03f924c464be5fd1ba44f8dc4c3cbc8162755fd1d8f7dc044b15b1a796c53df7d8769bb167b2045b49cc71e08908796c92c16a235717cabc4bb9f60f8f66ff4fff1f9836388a99583acebdff4a7fb20f48eedcd1f4bdcc06ec8b48e35307df51d9bc81d38a94992dd135b30079e1f592da6e98dff496cb1a7776460a26b06395b176f585636ebdf7eab692b227a31d6979f5a6141292698e91346b6c806b90c7c6971e481559cae92ee8f4136f2226861f5c39ddd29bbdb118a35dece03f49a96804caea79a3dacfbf09d65f2611b5622de51d98e18151acb3bb84c09caaa0cc80edfa743a4679f37d6167618ce99e73362fa6f213409931762618a61f1738c071bba5afc1db24fe94afb70c40d731908ab9a505f76f57a7d40e708fd3df0efc5b7cbb2a7b75cd23449e09684a2f0e2bfa0d6176c35f96fe94d92fc9fa4103972781f81cb6e8df7dbeb0fc529c600d768bed3f08828b773d284f69e9a203459d88c12d6df7a75be2455fec128f07a497a2b2bf626cc6272d0419ca663e9dc66b8224227eb796f0246dcae9c5b0b6cfdbbd40c3245a610481c92047c968c9fc92c04b89cc41a0c15355a8f", + "tlvs": { + "unknown_tag_1": "68656c6c6f", + "encrypted_recipient_data": "bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6" + } + } + ] + } +}