@@ -11,9 +11,19 @@ import (
1111 "github.com/stretchr/testify/require"
1212)
1313
14+ //nolint:lll
1415const (
15- routeBlindingTestFileName = "testdata/route-blinding-test.json"
16- onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json"
16+ routeBlindingTestFileName = "testdata/route-blinding-test.json"
17+ onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json"
18+ blindedOnionMessageOnionTestFileName = "testdata/blinded-onion-message-onion-test.json"
19+ )
20+
21+ var (
22+ // bolt4PubKeys contains the public keys used in the Bolt 4 spec. test
23+ // vectors. We convert them to variables named after the commonly used
24+ // names in cryptography.
25+ alicePubKey = bolt4PubKeys [0 ]
26+ bobPubKey = bolt4PubKeys [1 ]
1727)
1828
1929// TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against
@@ -117,6 +127,181 @@ func TestBuildBlindedRoute(t *testing.T) {
117127 }
118128}
119129
130+ // TestBuildOnionMessageBlindedRoute tests the construction of a blinded route
131+ // for an onion message, specifically the concatenation of two blinded paths,
132+ // against the spec. test vectors in `blinded-onion-message-onion-test.json`. It
133+ // verifies the correctness of BuildBlindedPath, decryptBlindedHopData, and
134+ // NextEphemeral.
135+ //
136+ // The test setup involves several parties and two distinct blinded paths that
137+ // are combined to form the full route:
138+ //
139+ // 1. Path from Dave: Dave (the receiver) first constructs a blinded path for a
140+ // message to be sent from Bob to himself (Dave).
141+ // The path is: Bob -> Carol -> Dave
142+ //
143+ // 2. Path from Sender: Dave gives his blinded path to a Sender. The Sender
144+ // then creates their own blinded path from themselves to Bob, passing
145+ // through Alice. The path is: Sender -> Alice -> Bob
146+ //
147+ // 3. Path Concatenation: The Sender prepends their path to Dave's path,
148+ // creating a final, concatenated route:
149+ // Sender -> Alice -> Bob -> Carol -> Dave
150+ // To link the two paths, the Sender includes a `next_path_key_override`
151+ // in the payload for Alice. This override is set to the first path key
152+ // (blinding point) of Dave's path, instructing Alice to use it for the next
153+ // hop (Bob) instead of the key that she could derive herself.
154+ //
155+ // The test then asserts that the generated concatenated path matches the test
156+ // vector's expected route. It simulates the decryption process at each hop,
157+ // verifying that at each hop encrypted_recipient_data is what we expect it to
158+ // be and that it correctly decrypts to the encrypted_data_tlv stream. Finally,
159+ // we verify the derivation of the next ephemeral key.
160+ func TestBuildOnionMessageBlindedRoute (t * testing.T ) {
161+ t .Parallel ()
162+
163+ // First, we'll read out the raw Json file at the target location.
164+ jsonBytes , err := os .ReadFile (blindedOnionMessageOnionTestFileName )
165+ require .NoError (t , err )
166+
167+ // Once we have the raw file, we'll unpack it into our
168+ // onionMessageJsonTestCase struct defined below.
169+ testCase := & onionMessageJsonTestCase {}
170+ require .NoError (t , json .Unmarshal (jsonBytes , testCase ))
171+ require .Len (t , testCase .Generate .Hops , 4 )
172+
173+ // buildMessagePath is a helper closure used to convert
174+ // hopOnionMessageData objects into HopInfo objects.
175+ buildMessagePath := func (h []hopOnionMessageData ,
176+ initialHopID string ) []* HopInfo {
177+
178+ path := make ([]* HopInfo , len (h ))
179+
180+ // The json test vector doesn't properly specify the current
181+ // node id, so we need the initial Node ID as a starting point.
182+ currentHop := initialHopID
183+ for i , hop := range h {
184+ nodeIDStr , err := hex .DecodeString (currentHop )
185+ require .NoError (t , err )
186+ nodeID , err := btcec .ParsePubKey (nodeIDStr )
187+ require .NoError (t , err )
188+ payload , err := hex .DecodeString (hop .EncryptedDataTlv )
189+ require .NoError (t , err )
190+
191+ path [i ] = & HopInfo {
192+ NodePub : nodeID ,
193+ PlainText : payload ,
194+ }
195+
196+ // The json test vector doesn't properly specify the
197+ // current node id. It does specify the next node id. So
198+ // to get the current node id for the next iteration, we
199+ // get the next node id here.
200+ currentHop = hop .EncodedOnionMessageTLVs .NextNodeID
201+ }
202+
203+ return path
204+ }
205+
206+ // First, Dave will build a blinded path from Bob to itself.
207+ receiverSessKey := privKeyFromString (
208+ testCase .Generate .Hops [1 ].PathKeySecret ,
209+ )
210+ daveBobPath := buildMessagePath (
211+ testCase .Generate .Hops [1 :], bobPubKey ,
212+ )
213+ daveBobBlindedPath , err := BuildBlindedPath (
214+ receiverSessKey , daveBobPath ,
215+ )
216+ require .NoError (t , err )
217+
218+ // At this point, Dave will give his blinded path to the Sender who will
219+ // then build its own blinded route from itself to Bob via Alice. The
220+ // sender will then concatenate the two paths. Note that in the payload
221+ // for Alice, the `next_path_key_override` field is added which is set
222+ // to the first path key in Dave's blinded route. This will indicate to
223+ // Alice that she should use this point for the next path key instead of
224+ // the next path key that she derives.
225+ // Path created by Dave: Bob -> Carol -> Dave
226+ // Path that the Sender will build: Sender -> Alice -> Bob
227+ aliceBobPath := buildMessagePath (
228+ testCase .Generate .Hops [:1 ], alicePubKey ,
229+ )
230+ senderSessKey := privKeyFromString (
231+ testCase .Generate .Hops [0 ].PathKeySecret ,
232+ )
233+ aliceBobBlindedPath , err := BuildBlindedPath (
234+ senderSessKey , aliceBobPath ,
235+ )
236+ require .NoError (t , err )
237+
238+ // Construct the concatenated path.
239+ path := & BlindedPath {
240+ IntroductionPoint : aliceBobBlindedPath .Path .IntroductionPoint ,
241+ BlindingPoint : aliceBobBlindedPath .Path .BlindingPoint ,
242+ BlindedHops : append (
243+ aliceBobBlindedPath .Path .BlindedHops ,
244+ daveBobBlindedPath .Path .BlindedHops ... ,
245+ ),
246+ }
247+
248+ // Check that the constructed path is equal to the test vector path.
249+ require .True (t , equalPubKeys (
250+ testCase .Route .FirstNodeId , path .IntroductionPoint ,
251+ ))
252+ require .True (t , equalPubKeys (
253+ testCase .Route .FirstPathKey , path .BlindingPoint ,
254+ ))
255+
256+ for i , hop := range testCase .Route .Hops {
257+ require .True (t , equalPubKeys (
258+ hop .BlindedNodeID , path .BlindedHops [i ].BlindedNodePub ,
259+ ))
260+
261+ data , _ := hex .DecodeString (hop .EncryptedRecipientData )
262+ require .Equal (t , data , path .BlindedHops [i ].CipherText )
263+ }
264+
265+ // Assert that each hop is able to decode the encrypted data meant for
266+ // it.
267+ for i , hop := range testCase .Decrypt .Hops {
268+ genData := testCase .Generate .Hops [i ]
269+ priv := privKeyFromString (hop .PrivKey )
270+ ephem := pubKeyFromString (genData .EphemeralPubKey )
271+
272+ // Check if the encrypted_recipient_data is what we expect it to
273+ // be.
274+ encRecipientDataExpected , err := hex .DecodeString (
275+ genData .EncryptedRecipientData ,
276+ )
277+ require .NoError (t , err )
278+ require .Equal (
279+ t , encRecipientDataExpected ,
280+ path .BlindedHops [i ].CipherText ,
281+ )
282+
283+ // Now we'll decrypt the blinded hop data using the private key
284+ // and the ephemeral public key.
285+ data , err := decryptBlindedHopData (
286+ & PrivKeyECDH {PrivKey : priv }, ephem ,
287+ path .BlindedHops [i ].CipherText ,
288+ )
289+ require .NoError (t , err )
290+
291+ // Check if the decrypted data is what we expect it to be.
292+ dataExpected , err := hex .DecodeString (genData .EncryptedDataTlv )
293+ require .NoError (t , err )
294+ require .Equal (t , dataExpected , data )
295+
296+ nextEphem , err := NextEphemeral (& PrivKeyECDH {priv }, ephem )
297+ require .NoError (t , err )
298+
299+ nextE := privKeyFromString (genData .NextEphemeralPrivKey )
300+
301+ require .Equal (t , nextE .PubKey (), nextEphem )
302+ }
303+ }
304+
120305// TestOnionRouteBlinding tests that an onion packet can correctly be processed
121306// by a node in a blinded route.
122307func TestOnionRouteBlinding (t * testing.T ) {
@@ -223,24 +408,48 @@ type decryptData struct {
223408 Hops []decryptHops `json:"hops"`
224409}
225410
411+ type decryptOnionMessageData struct {
412+ Hops []decryptOnionMessageHops `json:"hops"`
413+ }
414+
226415type decryptHops struct {
416+ //nolint:tagliatelle
227417 Onion string `json:"onion"`
228418 NodePrivKey string `json:"node_privkey"`
229419 NextBlinding string `json:"next_blinding"`
230420}
231421
422+ type decryptOnionMessageHops struct {
423+ OnionMessage string `json:"onion_message"`
424+ PrivKey string `json:"privkey"`
425+ NextNodeID string `json:"next_node_id"`
426+ }
427+
232428type blindingJsonTestCase struct {
233429 Generate generateData `json:"generate"`
234430 Route routeData `json:"route"`
235431 Unblind unblindData `json:"unblind"`
236432}
237433
434+ type onionMessageJsonTestCase struct {
435+ Generate generateOnionMessageData `json:"generate"`
436+ Route routeOnionMessageData `json:"route"`
437+ // OnionMessage onionMessageData `json:"onionmessage"`
438+ Decrypt decryptOnionMessageData `json:"decrypt"`
439+ }
440+
238441type routeData struct {
239442 IntroductionNodeID string `json:"introduction_node_id"`
240443 Blinding string `json:"blinding"`
241444 Hops []blindedHop `json:"hops"`
242445}
243446
447+ type routeOnionMessageData struct {
448+ FirstNodeId string `json:"first_node_id"`
449+ FirstPathKey string `json:"first_path_key"`
450+ Hops []blindedOnionMessageHop `json:"hops"`
451+ }
452+
244453type unblindData struct {
245454 Hops []unblindedHop `json:"hops"`
246455}
@@ -249,8 +458,14 @@ type generateData struct {
249458 Hops []hopData `json:"hops"`
250459}
251460
461+ type generateOnionMessageData struct {
462+ SessionKey string `json:"session_key"`
463+ Hops []hopOnionMessageData `json:"hops"`
464+ }
465+
252466type unblindedHop struct {
253- NodePrivKey string `json:"node_privkey"`
467+ NodePrivKey string `json:"node_privkey"`
468+ //nolint:tagliatelle
254469 EphemeralPubKey string `json:"ephemeral_pubkey"`
255470 DecryptedData string `json:"decrypted_data"`
256471 NextEphemeralPubKey string `json:"next_ephemeral_pubkey"`
@@ -262,11 +477,35 @@ type hopData struct {
262477 EncodedTLVs string `json:"encoded_tlvs"`
263478}
264479
480+ type hopOnionMessageData struct {
481+ PathKeySecret string `json:"path_key_secret"`
482+ EncodedOnionMessageTLVs encodedOnionMessageTLVs `json:"tlvs"`
483+ EncryptedDataTlv string `json:"encrypted_data_tlv"` //nolint:lll
484+ EphemeralPubKey string `json:"E"` //nolint:tagliatelle
485+ NextEphemeralPrivKey string `json:"next_e"`
486+ EncryptedRecipientData string `json:"encrypted_recipient_data"` //nolint:lll
487+ }
488+
489+ type encodedOnionMessageTLVs struct {
490+ NextNodeID string `json:"next_node_id"`
491+ NextPathKeyOverride string `json:"next_path_key_override"`
492+ PathKeyOverrideSecret string `json:"path_key_override_secret"`
493+ PathID string `json:"path_id"`
494+ // The test vector provides more fields, but since we don't want to pull
495+ // in the tlv package, they are omitted here. They should be tested in
496+ // higher layer tests.
497+ }
498+
265499type blindedHop struct {
266500 BlindedNodeID string `json:"blinded_node_id"`
267501 EncryptedData string `json:"encrypted_data"`
268502}
269503
504+ type blindedOnionMessageHop struct {
505+ BlindedNodeID string `json:"blinded_node_id"`
506+ EncryptedRecipientData string `json:"encrypted_recipient_data"`
507+ }
508+
270509func equalPubKeys (pkStr string , pk * btcec.PublicKey ) bool {
271510 return hex .EncodeToString (pk .SerializeCompressed ()) == pkStr
272511}
0 commit comments