Skip to content

Commit bce4aef

Browse files
committed
multi: decode zero-length onion message payloads
Since the onion message payload can be zero-length, we need to decode it correctly. This commit adds a boolean flag to the HopPayload Decode that tells whether the payload is an onion message payload or not. If it is, the payload is decoded as a tlv payload also if the first byte is 0x00. sphinx_test: Add zero-length payload om test
1 parent a82a397 commit bce4aef

File tree

3 files changed

+117
-31
lines changed

3 files changed

+117
-31
lines changed

payload.go

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,15 @@ func (hp *HopPayload) Decode(r io.Reader) error {
9999
return err
100100
}
101101

102-
var (
103-
legacyPayload = isLegacyPayloadByte(peekByte[0])
104-
payloadSize uint16
105-
)
102+
var payloadSize uint16
106103

107-
if legacyPayload {
104+
// If the HopPayload isn't guaranteed to be a TLV payload, we check the
105+
// first byte to see if it is a legacy payload.
106+
if hp.Type != PayloadTLV && isLegacyPayloadByte(peekByte[0]) {
108107
payloadSize = legacyPayloadSize()
109-
hp.Type = PayloadLegacy
110108
} else {
109+
// If the first byte doesn't indicate a legacy payload, then it
110+
// *must* be a TLV payload.
111111
payloadSize, err = tlvPayloadSize(bufReader)
112112
if err != nil {
113113
return err
@@ -116,19 +116,7 @@ func (hp *HopPayload) Decode(r io.Reader) error {
116116
hp.Type = PayloadTLV
117117
}
118118

119-
// Now that we know the payload size, we'll create a new buffer to
120-
// read it out in full.
121-
//
122-
// TODO(roasbeef): can avoid all these copies
123-
hp.Payload = make([]byte, payloadSize)
124-
if _, err := io.ReadFull(bufReader, hp.Payload[:]); err != nil {
125-
return err
126-
}
127-
if _, err := io.ReadFull(bufReader, hp.HMAC[:]); err != nil {
128-
return err
129-
}
130-
131-
return nil
119+
return readPayloadAndHMAC(hp, bufReader, payloadSize)
132120
}
133121

134122
// HopData attempts to extract a set of forwarding instructions from the target
@@ -146,6 +134,22 @@ func (hp *HopPayload) HopData() (*HopData, error) {
146134
return nil, nil
147135
}
148136

137+
// readPayloadAndHMAC reads the payload and HMAC from the reader into the
138+
// HopPayload.
139+
func readPayloadAndHMAC(hp *HopPayload, r io.Reader, payloadSize uint16) error {
140+
// Now that we know the payload size, we'll create a new buffer to read
141+
// it out in full.
142+
hp.Payload = make([]byte, payloadSize)
143+
if _, err := io.ReadFull(r, hp.Payload[:]); err != nil {
144+
return err
145+
}
146+
if _, err := io.ReadFull(r, hp.HMAC[:]); err != nil {
147+
return err
148+
}
149+
150+
return nil
151+
}
152+
149153
// tlvPayloadSize uses the passed reader to extract the payload length encoded
150154
// as a var-int.
151155
func tlvPayloadSize(r io.Reader) (uint16, error) {
@@ -314,8 +318,12 @@ func legacyNumBytes() int {
314318
return LegacyHopDataSize
315319
}
316320

317-
// isLegacyPayload returns true if the given byte is equal to the 0x00 byte
318-
// which indicates that the payload should be decoded as a legacy payload.
321+
// isLegacyPayloadByte determines if the first byte of a hop payload indicates
322+
// that it is a legacy payload. The first byte of a legacy payload will always
323+
// be 0x00, as this is the realm. For TLV payloads, the first byte is a
324+
// var-int encoding the length of the payload. A TLV stream can be empty, in
325+
// which case its length is 0, which is also encoded as a 0x00 byte. This
326+
// creates an ambiguity between a legacy payload and an empty TLV payload.
319327
func isLegacyPayloadByte(b byte) bool {
320328
return b == 0x00
321329
}

sphinx.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,8 @@ func (r *Router) Stop() {
510510
// processOnionCfg is a set of config values that can be used to modify how an
511511
// onion is processed.
512512
type processOnionCfg struct {
513-
blindingPoint *btcec.PublicKey
513+
blindingPoint *btcec.PublicKey
514+
tlvPayloadOnly bool
514515
}
515516

516517
// ProcessOnionOpt defines the signature of a function option that can be used
@@ -525,6 +526,14 @@ func WithBlindingPoint(point *btcec.PublicKey) ProcessOnionOpt {
525526
}
526527
}
527528

529+
// WithTLVPayloadOnly is a functional option that signals that the onion packet
530+
// being processed is from onion message.
531+
func WithTLVPayloadOnly() ProcessOnionOpt {
532+
return func(cfg *processOnionCfg) {
533+
cfg.tlvPayloadOnly = true
534+
}
535+
}
536+
528537
// ProcessOnionPacket processes an incoming onion packet which has been forward
529538
// to the target Sphinx router. If the encoded ephemeral key isn't on the
530539
// target Elliptic Curve, then the packet is rejected. Similarly, if the
@@ -560,7 +569,9 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte,
560569
// Continue to optimistically process this packet, deferring replay
561570
// protection until the end to reduce the penalty of multiple IO
562571
// operations.
563-
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData)
572+
packet, err := processOnionPacket(
573+
onionPkt, &sharedSecret, assocData, cfg.tlvPayloadOnly,
574+
)
564575
if err != nil {
565576
return nil, err
566577
}
@@ -594,7 +605,9 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte,
594605
return nil, err
595606
}
596607

597-
return processOnionPacket(onionPkt, &sharedSecret, assocData)
608+
return processOnionPacket(
609+
onionPkt, &sharedSecret, assocData, cfg.tlvPayloadOnly,
610+
)
598611
}
599612

600613
// DecryptBlindedHopData uses the router's private key to decrypt data encrypted
@@ -625,7 +638,8 @@ func (r *Router) OnionPublicKey() *btcec.PublicKey {
625638
// packet. This function returns the next inner onion packet layer, along with
626639
// the hop data extracted from the outer onion packet.
627640
func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
628-
assocData []byte) (*OnionPacket, *HopPayload, error) {
641+
assocData []byte, tlvPayloadOnly bool) (*OnionPacket, *HopPayload,
642+
error) {
629643

630644
dhKey := onionPkt.EphemeralKey
631645
routeInfo := onionPkt.RoutingInfo
@@ -660,8 +674,16 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
660674
// With the MAC checked, and the payload decrypted, we can now parse
661675
// out the payload so we can derive the specified forwarding
662676
// instructions.
663-
var hopPayload HopPayload
664-
if err := hopPayload.Decode(bytes.NewReader(hopInfo[:])); err != nil {
677+
hopPayload := HopPayload{}
678+
if tlvPayloadOnly {
679+
// If the payload is assured to be TLV, we don't have to support
680+
// legacy payloads, but we do support zero-length payloads. By
681+
// specifically setting the type to TLV, we ensure that the
682+
// payload is treated as such.
683+
hopPayload.Type = PayloadTLV
684+
}
685+
err := hopPayload.Decode(bytes.NewReader(hopInfo[:]))
686+
if err != nil {
665687
return nil, nil, err
666688
}
667689

@@ -683,7 +705,7 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
683705
// packets. The processed packets returned from this method should only be used
684706
// if the packet was not flagged as a replayed packet.
685707
func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
686-
assocData []byte) (*ProcessedPacket, error) {
708+
assocData []byte, tlvPayloadOnly bool) (*ProcessedPacket, error) {
687709

688710
// First, we'll unwrap an initial layer of the onion packet. Typically,
689711
// we'll only have a single layer to unwrap, However, if the sender has
@@ -693,7 +715,7 @@ func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
693715
// they can properly check the HMAC and unwrap a layer for their
694716
// handoff hop.
695717
innerPkt, outerHopPayload, err := unwrapPacket(
696-
onionPkt, sharedSecret, assocData,
718+
onionPkt, sharedSecret, assocData, tlvPayloadOnly,
697719
)
698720
if err != nil {
699721
return nil, err
@@ -703,7 +725,7 @@ func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
703725
// However if the uncovered 'nextMac' is all zeroes, then this
704726
// indicates that we're the final hop in the route.
705727
var action ProcessCode = MoreHops
706-
if bytes.Compare(zeroHMAC[:], outerHopPayload.HMAC[:]) == 0 {
728+
if bytes.Equal(zeroHMAC[:], outerHopPayload.HMAC[:]) {
707729
action = ExitNode
708730
}
709731

@@ -794,7 +816,9 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
794816
// Continue to optimistically process this packet, deferring replay
795817
// protection until the end to reduce the penalty of multiple IO
796818
// operations.
797-
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData)
819+
packet, err := processOnionPacket(
820+
onionPkt, &sharedSecret, assocData, cfg.tlvPayloadOnly,
821+
)
798822
if err != nil {
799823
return err
800824
}

sphinx_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,60 @@ func TestTLVPayloadMessagePacket(t *testing.T) {
288288
hex.EncodeToString(finalPacket), hex.EncodeToString(b.Bytes()))
289289
}
290290

291+
// TestProcessOnionMessageZeroLengthPayload tests that we can properly process an
292+
// onion message that has a zero-length payload.
293+
func TestProcessOnionMessageZeroLengthPayload(t *testing.T) {
294+
t.Parallel()
295+
296+
// First, create a router that will be the destination of the onion
297+
// message.
298+
privKey, err := btcec.NewPrivateKey()
299+
require.NoError(t, err)
300+
301+
router := NewRouter(&PrivKeyECDH{privKey}, NewMemoryReplayLog())
302+
err = router.Start()
303+
require.NoError(t, err)
304+
defer router.Stop()
305+
306+
// Next, create a session key for the onion packet.
307+
sessionKey, err := btcec.NewPrivateKey()
308+
require.NoError(t, err)
309+
310+
// We'll create a simple one-hop path.
311+
path := &PaymentPath{
312+
{
313+
NodePub: *privKey.PubKey(),
314+
},
315+
}
316+
317+
// The hop payload will be an empty TLV payload.
318+
payload, err := NewTLVHopPayload(nil)
319+
require.NoError(t, err)
320+
path[0].HopPayload = payload
321+
322+
// Now, create the onion packet.
323+
onionPacket, err := NewOnionPacket(
324+
path, sessionKey, nil, DeterministicPacketFiller,
325+
)
326+
require.NoError(t, err)
327+
328+
// We'll now process the packet, making sure to indicate that this is
329+
// an onion message.
330+
processedPacket, err := router.ProcessOnionPacket(
331+
onionPacket, nil, 0, WithTLVPayloadOnly(),
332+
)
333+
require.NoError(t, err)
334+
335+
// The packet should be decoded as an exit node.
336+
require.EqualValues(t, ExitNode, processedPacket.Action)
337+
338+
// The payload should be of type TLV.
339+
require.Equal(t, PayloadTLV, processedPacket.Payload.Type)
340+
341+
// And the payload should be empty.
342+
require.Empty(t, processedPacket.Payload.Payload)
343+
}
344+
291345
func TestSphinxCorrectness(t *testing.T) {
292346
nodes, _, hopDatas, fwdMsg, err := newTestRoute(testLegacyRouteNumHops)
293347
if err != nil {

0 commit comments

Comments
 (0)