Skip to content

Commit 75af2ed

Browse files
committed
sphinx_test: add blinded onion message test
This commit adds the spec test vector for blinded onion messages. It also adds a test that tests BuildBlindedRoute, decryptBlindedHopData and NextEphemeral against this vector.
1 parent 93b627b commit 75af2ed

File tree

2 files changed

+385
-3
lines changed

2 files changed

+385
-3
lines changed

path_test.go

Lines changed: 242 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@ import (
1111
"github.com/stretchr/testify/require"
1212
)
1313

14+
//nolint:lll
1415
const (
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.
122307
func 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+
226415
type 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+
232428
type 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+
238441
type 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+
244453
type 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+
252466
type 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+
265499
type 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+
270509
func equalPubKeys(pkStr string, pk *btcec.PublicKey) bool {
271510
return hex.EncodeToString(pk.SerializeCompressed()) == pkStr
272511
}

0 commit comments

Comments
 (0)