Skip to content

Commit b1469b8

Browse files
committed
multi: handle route blinding in ProcessOnionPacket
In this commit, ProcessOnionPacket is updated to take in a blinding key as a parameter. This key is then used in order to determine the blinding factor necessary to decrypt the onion. This change is accompanied with a test that tests this change against a test vector from the route blinding spec PR.
1 parent e9cb480 commit b1469b8

File tree

7 files changed

+441
-41
lines changed

7 files changed

+441
-41
lines changed

bench_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ func BenchmarkProcessPacket(b *testing.B) {
7777
pkt *ProcessedPacket
7878
)
7979
for i := 0; i < b.N; i++ {
80-
pkt, err = path[0].ProcessOnionPacket(sphinxPacket, nil, uint32(i))
80+
pkt, err = path[0].ProcessOnionPacket(
81+
sphinxPacket, nil, uint32(i),
82+
)
8183
if err != nil {
8284
b.Fatalf("unable to process packet %d: %v", i, err)
8385
}

crypto.go

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,17 +239,63 @@ type sharedSecretGenerator interface {
239239
generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error)
240240
}
241241

242-
// generateSharedSecret generates the shared secret by given ephemeral key.
243-
func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) {
242+
// generateSharedSecret generates the shared secret using the given ephemeral
243+
// pub key and the Router's private key. If a blindingPoint is provided then it
244+
// is used to tweak the Router's private key before creating the shared secret
245+
// with the ephemeral pub key. The blinding point is used to determine our
246+
// shared secret with the receiver. From that we can determine our shared
247+
// secret with the sender using the dhKey.
248+
func (r *Router) generateSharedSecret(dhKey,
249+
blindingPoint *btcec.PublicKey) (Hash256, error) {
250+
251+
// If no blinding point is provided, then the un-tweaked dhKey can
252+
// be used to derive the shared secret
253+
if blindingPoint == nil {
254+
return sharedSecret(r.onionKey, dhKey)
255+
}
256+
257+
// We use the blinding point to calculate the blinding factor that the
258+
// receiver used with us so that we can use it to tweak our priv key.
259+
// The sender would have created their shared secret with our blinded
260+
// pub key.
261+
// * ss_receiver = H(k * E_receiver)
262+
ssReceiver, err := sharedSecret(r.onionKey, blindingPoint)
263+
if err != nil {
264+
return Hash256{}, err
265+
}
266+
267+
// Compute the blinding factor that the receiver would have used to
268+
// blind our public key.
269+
//
270+
// * bf = HMAC256("blinded_node_id", ss_receiver)
271+
blindingFactorBytes := generateKey(routeBlindingHMACKey, &ssReceiver)
272+
var blindingFactor btcec.ModNScalar
273+
blindingFactor.SetBytes(&blindingFactorBytes)
274+
275+
// Now, we want to calculate the shared secret between the sender and
276+
// our blinded key. In other words we want to calculate:
277+
// * ss_sender = H(E_sender * bf * k)
278+
//
279+
// Since the order in which the above multiplication happens does not
280+
// matter, we will first multiply E_sender with the blinding factor:
281+
blindedEphemeral := blindGroupElement(dhKey, blindingFactor)
282+
283+
// Finally, we compute the ECDH to get the shared secret, ss_sender:
284+
return sharedSecret(r.onionKey, blindedEphemeral)
285+
}
286+
287+
// sharedSecret does a ECDH operation on the passed private and public keys and
288+
// returns the result.
289+
func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) {
244290
var sharedSecret Hash256
245291

246292
// Ensure that the public key is on our curve.
247-
if !dhKey.IsOnCurve() {
293+
if !pub.IsOnCurve() {
248294
return sharedSecret, ErrInvalidOnionKey
249295
}
250296

251-
// Compute our shared secret.
252-
return r.onionKey.ECDH(dhKey)
297+
// Compute the shared secret.
298+
return priv.ECDH(pub)
253299
}
254300

255301
// onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a

obfuscation.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ type OnionErrorEncrypter struct {
1313
}
1414

1515
// NewOnionErrorEncrypter creates new instance of the onion encrypter backed by
16-
// the passed router, with encryption to be doing using the passed
17-
// ephemeralKey.
18-
func NewOnionErrorEncrypter(router *Router,
19-
ephemeralKey *btcec.PublicKey) (*OnionErrorEncrypter, error) {
16+
// the passed router, with encryption to be done using the passed ephemeralKey.
17+
func NewOnionErrorEncrypter(router *Router, ephemeralKey *btcec.PublicKey,
18+
opts ...ProcessOnionOpt) (*OnionErrorEncrypter, error) {
2019

21-
sharedSecret, err := router.generateSharedSecret(ephemeralKey)
20+
cfg := &processOnionCfg{}
21+
for _, o := range opts {
22+
o(cfg)
23+
}
24+
25+
sharedSecret, err := router.generateSharedSecret(
26+
ephemeralKey, cfg.blindingPoint,
27+
)
2228
if err != nil {
2329
return nil, err
2430
}

path_test.go

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ import (
88
"testing"
99

1010
"github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/btcsuite/btcd/chaincfg"
1112
"github.com/stretchr/testify/require"
1213
)
1314

14-
const routeBlindingTestFileName = "testdata/route-blinding-test.json"
15+
const (
16+
routeBlindingTestFileName = "testdata/route-blinding-test.json"
17+
onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json"
18+
)
1519

1620
// TestBuildBlindedRoute tests BuildBlindedRoute and decryptBlindedHopData against
1721
// the spec test vectors.
@@ -114,6 +118,119 @@ func TestBuildBlindedRoute(t *testing.T) {
114118
}
115119
}
116120

121+
// TestOnionRouteBlinding tests that an onion packet can correctly be processed
122+
// by a node in a blinded route.
123+
func TestOnionRouteBlinding(t *testing.T) {
124+
t.Parallel()
125+
126+
// First, we'll read out the raw Json file at the target location.
127+
jsonBytes, err := os.ReadFile(onionRouteBlindingTestFileName)
128+
require.NoError(t, err)
129+
130+
// Once we have the raw file, we'll unpack it into our
131+
// blindingJsonTestCase struct defined above.
132+
testCase := &onionBlindingJsonTestCase{}
133+
require.NoError(t, json.Unmarshal(jsonBytes, testCase))
134+
135+
assoc, err := hex.DecodeString(testCase.Generate.AssocData)
136+
require.NoError(t, err)
137+
138+
// Extract the original onion packet to be processed.
139+
onion, err := hex.DecodeString(testCase.Generate.Onion)
140+
require.NoError(t, err)
141+
142+
onionBytes := bytes.NewReader(onion)
143+
onionPacket := &OnionPacket{}
144+
require.NoError(t, onionPacket.Decode(onionBytes))
145+
146+
// peelOnion is a helper closure that can be used to set up a Router
147+
// and use it to process the given onion packet.
148+
peelOnion := func(key *btcec.PrivateKey,
149+
blindingPoint *btcec.PublicKey) *ProcessedPacket {
150+
151+
r := NewRouter(
152+
&PrivKeyECDH{PrivKey: key}, &chaincfg.MainNetParams,
153+
NewMemoryReplayLog(),
154+
)
155+
156+
require.NoError(t, r.Start())
157+
defer r.Stop()
158+
159+
res, err := r.ProcessOnionPacket(
160+
onionPacket, assoc, 10,
161+
WithBlindingPoint(blindingPoint),
162+
)
163+
require.NoError(t, err)
164+
165+
return res
166+
}
167+
168+
hops := testCase.Decrypt.Hops
169+
require.Len(t, hops, 5)
170+
171+
// There are some things that the processor of the onion packet will
172+
// only be able to determine from the actual contents of the encrypted
173+
// data it receives. These things include the next_blinding_point for
174+
// the introduction point and the next_blinding_override. The decryption
175+
// of this data is dependent on the encoding chosen by higher layers.
176+
// The test uses TLVs. Since the extraction of this data is dependent
177+
// on layers outside the scope of this library, we provide handle these
178+
// cases manually for the sake of the test.
179+
var (
180+
introPointIndex = 2
181+
firstBlinding = pubKeyFromString(hops[1].NextBlinding)
182+
183+
concatIndex = 3
184+
blindingOverride = pubKeyFromString(hops[2].NextBlinding)
185+
)
186+
187+
var blindingPoint *btcec.PublicKey
188+
for i, hop := range testCase.Decrypt.Hops {
189+
buff := bytes.NewBuffer(nil)
190+
require.NoError(t, onionPacket.Encode(buff))
191+
require.Equal(t, hop.Onion, hex.EncodeToString(buff.Bytes()))
192+
193+
priv := privKeyFromString(hop.NodePrivKey)
194+
195+
if i == introPointIndex {
196+
blindingPoint = firstBlinding
197+
} else if i == concatIndex {
198+
blindingPoint = blindingOverride
199+
}
200+
201+
processedPkt := peelOnion(priv, blindingPoint)
202+
203+
if blindingPoint != nil {
204+
blindingPoint, err = NextEphemeral(
205+
&PrivKeyECDH{priv}, blindingPoint,
206+
)
207+
require.NoError(t, err)
208+
}
209+
onionPacket = processedPkt.NextPacket
210+
}
211+
}
212+
213+
type onionBlindingJsonTestCase struct {
214+
Generate generateOnionData `json:"generate"`
215+
Decrypt decryptData `json:"decrypt"`
216+
}
217+
218+
type generateOnionData struct {
219+
SessionKey string `json:"session_key"`
220+
AssocData string `json:"associated_data"`
221+
Onion string `json:"onion"`
222+
}
223+
224+
type decryptData struct {
225+
Hops []decryptHops `json:"hops"`
226+
}
227+
228+
type decryptHops struct {
229+
Onion string `json:"onion"`
230+
NodePrivKey string `json:"node_privkey"`
231+
NextBlinding string `json:"next_blinding"`
232+
}
233+
117234
type blindingJsonTestCase struct {
118235
Generate generateData `json:"generate"`
119236
Route routeData `json:"route"`

sphinx.go

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -521,20 +521,48 @@ func (r *Router) Stop() {
521521
r.log.Stop()
522522
}
523523

524+
// processOnionCfg is a set of config values that can be used to modify how an
525+
// onion is processed.
526+
type processOnionCfg struct {
527+
blindingPoint *btcec.PublicKey
528+
}
529+
530+
// ProcessOnionOpt defines the signature of a function option that can be used
531+
// to modify how an onion is processed.
532+
type ProcessOnionOpt func(cfg *processOnionCfg)
533+
534+
// WithBlindingPoint is a function option that can be used to set the blinding
535+
// point to be used when processing an onion.
536+
func WithBlindingPoint(point *btcec.PublicKey) ProcessOnionOpt {
537+
return func(cfg *processOnionCfg) {
538+
cfg.blindingPoint = point
539+
}
540+
}
541+
524542
// ProcessOnionPacket processes an incoming onion packet which has been forward
525543
// to the target Sphinx router. If the encoded ephemeral key isn't on the
526544
// target Elliptic Curve, then the packet is rejected. Similarly, if the
527-
// derived shared secret has been seen before the packet is rejected. Finally
528-
// if the MAC doesn't check the packet is again rejected.
545+
// derived shared secret has been seen before the packet is rejected. If the
546+
// blinded point is specified, then it will be used along with the ephemeral key
547+
// in the onion packet to derive the shared secret. Finally, if the MAC doesn't
548+
// check the packet is again rejected.
529549
//
530550
// In the case of a successful packet processing, and ProcessedPacket struct is
531551
// returned which houses the newly parsed packet, along with instructions on
532552
// what to do next.
533-
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket,
534-
assocData []byte, incomingCltv uint32) (*ProcessedPacket, error) {
553+
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte,
554+
incomingCltv uint32, opts ...ProcessOnionOpt) (*ProcessedPacket,
555+
error) {
556+
557+
cfg := &processOnionCfg{}
558+
for _, o := range opts {
559+
o(cfg)
560+
}
535561

536562
// Compute the shared secret for this onion packet.
537-
sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey)
563+
sharedSecret, err := r.generateSharedSecret(
564+
onionPkt.EphemeralKey, cfg.blindingPoint,
565+
)
538566
if err != nil {
539567
return nil, err
540568
}
@@ -546,7 +574,7 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket,
546574
// Continue to optimistically process this packet, deferring replay
547575
// protection until the end to reduce the penalty of multiple IO
548576
// operations.
549-
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData, r)
577+
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData)
550578
if err != nil {
551579
return nil, err
552580
}
@@ -564,16 +592,23 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket,
564592
//
565593
// NOTE: This method does not do any sort of replay protection, and should only
566594
// be used to reconstruct packets that were successfully processed previously.
567-
func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket,
568-
assocData []byte) (*ProcessedPacket, error) {
595+
func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte,
596+
opts ...ProcessOnionOpt) (*ProcessedPacket, error) {
597+
598+
cfg := &processOnionCfg{}
599+
for _, o := range opts {
600+
o(cfg)
601+
}
569602

570603
// Compute the shared secret for this onion packet.
571-
sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey)
604+
sharedSecret, err := r.generateSharedSecret(
605+
onionPkt.EphemeralKey, cfg.blindingPoint,
606+
)
572607
if err != nil {
573608
return nil, err
574609
}
575610

576-
return processOnionPacket(onionPkt, &sharedSecret, assocData, r)
611+
return processOnionPacket(onionPkt, &sharedSecret, assocData)
577612
}
578613

579614
// unwrapPacket wraps a layer of the passed onion packet using the specified
@@ -640,8 +675,7 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
640675
// packets. The processed packets returned from this method should only be used
641676
// if the packet was not flagged as a replayed packet.
642677
func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
643-
assocData []byte,
644-
sharedSecretGen sharedSecretGenerator) (*ProcessedPacket, error) {
678+
assocData []byte) (*ProcessedPacket, error) {
645679

646680
// First, we'll unwrap an initial layer of the onion packet. Typically,
647681
// we'll only have a single layer to unwrap, However, if the sender has
@@ -721,18 +755,25 @@ func (r *Router) BeginTxn(id []byte, nels int) *Tx {
721755
// ProcessOnionPacket processes an incoming onion packet which has been forward
722756
// to the target Sphinx router. If the encoded ephemeral key isn't on the
723757
// target Elliptic Curve, then the packet is rejected. Similarly, if the
724-
// derived shared secret has been seen before the packet is rejected. Finally
725-
// if the MAC doesn't check the packet is again rejected.
758+
// derived shared secret has been seen before the packet is rejected. If the
759+
// blinded point is specified, then it will be used along with the ephemeral key
760+
// in the onion packet to derive the shared secret. Finally, if the MAC doesn't
761+
// check the packet is again rejected.
726762
//
727763
// In the case of a successful packet processing, and ProcessedPacket struct is
728764
// returned which houses the newly parsed packet, along with instructions on
729765
// what to do next.
730766
func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
731-
assocData []byte, incomingCltv uint32) error {
767+
assocData []byte, incomingCltv uint32, opts ...ProcessOnionOpt) error {
768+
769+
cfg := &processOnionCfg{}
770+
for _, o := range opts {
771+
o(cfg)
772+
}
732773

733774
// Compute the shared secret for this onion packet.
734775
sharedSecret, err := t.router.generateSharedSecret(
735-
onionPkt.EphemeralKey,
776+
onionPkt.EphemeralKey, cfg.blindingPoint,
736777
)
737778
if err != nil {
738779
return err
@@ -745,9 +786,7 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
745786
// Continue to optimistically process this packet, deferring replay
746787
// protection until the end to reduce the penalty of multiple IO
747788
// operations.
748-
packet, err := processOnionPacket(
749-
onionPkt, &sharedSecret, assocData, t.router,
750-
)
789+
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData)
751790
if err != nil {
752791
return err
753792
}

0 commit comments

Comments
 (0)