Skip to content

Commit d48ed41

Browse files
committed
multi: Support jumbo size om packets
Onion messages allow for payloads that exceed 1300 bytes, in which case the payload should become 32768 bytes. This commit introduces support for those jumbo packets. sphinx_test: test jumbo size onion message packets This commit adds a helper function to create onion messages of a specified length. This helper is then used to test the handling of packets larger than 1300 bytes specifically for onion messages.
1 parent da7260e commit d48ed41

File tree

5 files changed

+283
-64
lines changed

5 files changed

+283
-64
lines changed

bench_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func BenchmarkPathPacketConstruction(b *testing.B) {
5252

5353
for i := 0; i < b.N; i++ {
5454
sphinxPacket, err = NewOnionPacket(
55-
&route, d, nil, BlankPacketFiller,
55+
&route, d, nil, BlankPacketFiller, false,
5656
)
5757
if err != nil {
5858
b.Fatalf("unable to create packet: %v", err)

cmd/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ func main() {
7070
"data.",
7171
Value: defaultHopDataPath,
7272
},
73+
cli.BoolFlag{
74+
Name: "onion-message",
75+
Usage: "Create an onion message " +
76+
"packet rather than a " +
77+
"payment onion.",
78+
},
7379
},
7480
},
7581
{
@@ -205,6 +211,7 @@ func generate(ctx *cli.Context) error {
205211

206212
msg, err := sphinx.NewOnionPacket(
207213
path, sessionKey, assocData, sphinx.DeterministicPacketFiller,
214+
ctx.Bool("onion-message"),
208215
)
209216
if err != nil {
210217
return fmt.Errorf("error creating message: %v", err)

packetfiller.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ import (
1212
// in order to ensure we don't leak information on the true route length to the
1313
// receiver. The packet filler may also use the session key to generate a set
1414
// of filler bytes if it wishes to be deterministic.
15-
type PacketFiller func(*btcec.PrivateKey, *[routingInfoSize]byte) error
15+
type PacketFiller func(*btcec.PrivateKey, []byte) error
1616

1717
// RandPacketFiller is a packet filler that reads a set of random bytes from a
1818
// CSPRNG.
19-
func RandPacketFiller(_ *btcec.PrivateKey, mixHeader *[routingInfoSize]byte) error {
19+
func RandPacketFiller(_ *btcec.PrivateKey, mixHeader []byte) error {
2020
// Read out random bytes to fill out the rest of the starting packet
2121
// after the hop payload for the final node. This mitigates a privacy
2222
// leak that may reveal a lower bound on the true path length to the
2323
// receiver.
24-
if _, err := rand.Read(mixHeader[:]); err != nil {
24+
if _, err := rand.Read(mixHeader); err != nil {
2525
return err
2626
}
2727

@@ -31,15 +31,15 @@ func RandPacketFiller(_ *btcec.PrivateKey, mixHeader *[routingInfoSize]byte) err
3131
// BlankPacketFiller is a packet filler that doesn't attempt to fill out the
3232
// packet at all. It should ONLY be used for generating test vectors or other
3333
// instances that required deterministic packet generation.
34-
func BlankPacketFiller(_ *btcec.PrivateKey, _ *[routingInfoSize]byte) error {
34+
func BlankPacketFiller(_ *btcec.PrivateKey, _ []byte) error {
3535
return nil
3636
}
3737

3838
// DeterministicPacketFiller is a packet filler that generates a deterministic
3939
// set of filler bytes by using chacha20 with a key derived from the session
4040
// key.
4141
func DeterministicPacketFiller(sessionKey *btcec.PrivateKey,
42-
mixHeader *[routingInfoSize]byte) error {
42+
mixHeader []byte) error {
4343

4444
// First, we'll generate a new key that'll be used to generate some
4545
// random bytes for our padding purposes. To derive this new key, we
@@ -55,7 +55,7 @@ func DeterministicPacketFiller(sessionKey *btcec.PrivateKey,
5555
if err != nil {
5656
return err
5757
}
58-
padCipher.XORKeyStream(mixHeader[:], mixHeader[:])
58+
padCipher.XORKeyStream(mixHeader, mixHeader)
5959

6060
return nil
6161
}

sphinx.go

Lines changed: 108 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,26 @@ const (
4141
LegacyHopDataSize = (RealmByteSize + AddressSize + AmtForwardSize +
4242
OutgoingCLTVSize + NumPaddingBytes + HMACSize)
4343

44-
// MaxPayloadSize is the maximum size a payload for a single hop can be.
45-
// This is the worst case scenario of a single hop, consuming all
46-
// available space. We need to know this in order to generate a
47-
// sufficiently long stream of pseudo-random bytes when
48-
// encrypting/decrypting the payload.
49-
MaxPayloadSize = routingInfoSize
50-
51-
// routingInfoSize is the fixed size of the the routing info. This
52-
// consists of a addressSize byte address and a HMACSize byte HMAC for
44+
// MaxPayloadSize is the maximum size an `update_add_htlc` payload for a
45+
// single hop can be. This is the worst case scenario of a single hop,
46+
// consuming all available space. We need to know this in order to
47+
// generate a sufficiently long stream of pseudo-random bytes when
48+
// encrypting/decrypting the payload. This field is here for backwards
49+
// compatibility. Throughout the code we use StandardRoutingInfoSize
50+
// because of the more apt naming.
51+
MaxPayloadSize = standardRoutingInfoSize
52+
StandardRoutingInfoSize = standardRoutingInfoSize
53+
54+
// standardRoutingInfoSize is the fixed size of the the routing info. This
55+
// consists of an addressSize byte address and a HMACSize byte HMAC for
5356
// each hop of the route, the first pair in cleartext and the following
5457
// pairs increasingly obfuscated. If not all space is used up, the
5558
// remainder is padded with null-bytes, also obfuscated.
56-
routingInfoSize = 1300
59+
standardRoutingInfoSize = 1300
5760

58-
// numStreamBytes is the number of bytes produced by our CSPRG for the
59-
// key stream implementing our stream cipher to encrypt/decrypt the mix
60-
// header. The MaxPayloadSize bytes at the end are used to
61-
// encrypt/decrypt the fillers when processing the packet of generating
62-
// the HMACs when creating the packet.
63-
numStreamBytes = routingInfoSize * 2
61+
// JumboRoutingInfoSize is the size of the routing info for a jumbo
62+
// onion packet.
63+
JumboRoutingInfoSize = 32768
6464

6565
// keyLen is the length of the keys used to generate cipher streams and
6666
// encrypt payloads. Since we use SHA256 to generate the keys, the
@@ -72,8 +72,15 @@ const (
7272
)
7373

7474
var (
75-
ErrMaxRoutingInfoSizeExceeded = fmt.Errorf(
76-
"max routing info size of %v bytes exceeded", routingInfoSize)
75+
ErrStandardRoutingPayloadSizeExceeded = fmt.Errorf(
76+
"max routing info size of %v bytes exceeded",
77+
StandardRoutingInfoSize,
78+
)
79+
80+
ErrMessageRoutingPayloadSizeExceeded = fmt.Errorf(
81+
"max onion message routing info size of %v bytes exceeded",
82+
JumboRoutingInfoSize,
83+
)
7784
)
7885

7986
// OnionPacket is the onion wrapped hop-to-hop routing information necessary to
@@ -102,7 +109,7 @@ type OnionPacket struct {
102109
// RoutingInfo is the full routing information for this onion packet.
103110
// This encodes all the forwarding instructions for this current hop
104111
// and all the hops in the route.
105-
RoutingInfo [routingInfoSize]byte
112+
RoutingInfo []byte
106113

107114
// HeaderMAC is an HMAC computed with the shared secret of the routing
108115
// data and the associated data for this route. Including the
@@ -193,12 +200,8 @@ func generateSharedSecrets(paymentPath []*btcec.PublicKey,
193200
// NewOnionPacket creates a new onion packet which is capable of obliviously
194201
// routing a message through the mix-net path outline by 'paymentPath'.
195202
func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
196-
assocData []byte, pktFiller PacketFiller) (*OnionPacket, error) {
197-
198-
// Check whether total payload size doesn't exceed the hard maximum.
199-
if paymentPath.TotalPayloadSize() > routingInfoSize {
200-
return nil, ErrMaxRoutingInfoSizeExceeded
201-
}
203+
assocData []byte, pktFiller PacketFiller,
204+
isOnionMessage bool) (*OnionPacket, error) {
202205

203206
// If we don't actually have a partially populated route, then we'll
204207
// exit early.
@@ -207,6 +210,34 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
207210
return nil, fmt.Errorf("route of length zero passed in")
208211
}
209212

213+
totalPayloadSize := paymentPath.TotalPayloadSize()
214+
215+
routingInfoLen := StandardRoutingInfoSize
216+
maxRoutingInfoErr := ErrStandardRoutingPayloadSizeExceeded
217+
if isOnionMessage && totalPayloadSize > StandardRoutingInfoSize {
218+
routingInfoLen = JumboRoutingInfoSize
219+
maxRoutingInfoErr = ErrMessageRoutingPayloadSizeExceeded
220+
}
221+
222+
// Check whether total payload size doesn't exceed the hard maximum.
223+
if totalPayloadSize > routingInfoLen {
224+
return nil, maxRoutingInfoErr
225+
}
226+
227+
// Before we proceed, we'll check that the payload types of each hop
228+
// in the payment path match the type of onion packet we're creating.
229+
for i := 0; i < numHops; i++ {
230+
hopPayload := (*paymentPath)[i].HopPayload
231+
isLegacy := hopPayload.Type == PayloadLegacy
232+
233+
// If this is an onion message, we only expect TLV
234+
// payloads.
235+
if isOnionMessage && isLegacy {
236+
return nil, fmt.Errorf("hop %d has legacy payload, "+
237+
"but onion messages require TLV", i)
238+
}
239+
}
240+
210241
// We'll force the caller to provide a packet filler, as otherwise we
211242
// may default to an insecure filling method (which should only really
212243
// be used to generate test vectors).
@@ -222,18 +253,20 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
222253
}
223254

224255
// Generate the padding, called "filler strings" in the paper.
225-
filler := generateHeaderPadding("rho", paymentPath, hopSharedSecrets)
256+
filler := generateHeaderPadding(
257+
"rho", paymentPath, hopSharedSecrets, routingInfoLen,
258+
)
226259

227260
// Allocate zero'd out byte slices to store the final mix header packet
228261
// and the hmac for each hop.
229262
var (
230-
mixHeader [routingInfoSize]byte
263+
mixHeader = make([]byte, routingInfoLen)
231264
nextHmac [HMACSize]byte
232265
hopPayloadBuf bytes.Buffer
233266
)
234267

235268
// Fill the packet using the caller specified methodology.
236-
if err := pktFiller(sessionKey, &mixHeader); err != nil {
269+
if err := pktFiller(sessionKey, mixHeader); err != nil {
237270
return nil, err
238271
}
239272

@@ -254,26 +287,26 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
254287
// Next, using the key dedicated for our stream cipher, we'll
255288
// generate enough bytes to obfuscate this layer of the onion
256289
// packet.
257-
streamBytes := generateCipherStream(rhoKey, routingInfoSize)
290+
streamBytes := generateCipherStream(rhoKey, uint(routingInfoLen))
258291
payload := paymentPath[i].HopPayload
259292

260293
// Before we assemble the packet, we'll shift the current
261294
// mix-header to the right in order to make room for this next
262295
// per-hop data.
263296
shiftSize := payload.NumBytes()
264-
rightShift(mixHeader[:], shiftSize)
297+
rightShift(mixHeader, shiftSize)
265298

266299
err := payload.Encode(&hopPayloadBuf)
267300
if err != nil {
268301
return nil, err
269302
}
270303

271-
copy(mixHeader[:], hopPayloadBuf.Bytes())
304+
copy(mixHeader, hopPayloadBuf.Bytes())
272305

273306
// Once the packet for this hop has been assembled, we'll
274307
// re-encrypt the packet by XOR'ing with a stream of bytes
275308
// generated using our shared secret.
276-
xor(mixHeader[:], mixHeader[:], streamBytes[:])
309+
xor(mixHeader, mixHeader, streamBytes)
277310

278311
// If this is the "last" hop, then we'll override the tail of
279312
// the hop data.
@@ -285,7 +318,7 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
285318
// calculating the MAC, we'll also include the optional
286319
// associated data which can allow higher level applications to
287320
// prevent replay attacks.
288-
packet := append(mixHeader[:], assocData...)
321+
packet := append(mixHeader, assocData...)
289322
nextHmac = calcMac(muKey, packet)
290323

291324
hopPayloadBuf.Reset()
@@ -322,7 +355,9 @@ func rightShift(slice []byte, num int) {
322355
// leaving only the original "filler" bytes produced by this function at the
323356
// last hop. Using this methodology, the size of the field stays constant at
324357
// each hop.
325-
func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash256) []byte {
358+
func generateHeaderPadding(key string, path *PaymentPath,
359+
sharedSecrets []Hash256, routingInfoLen int) []byte {
360+
326361
numHops := path.TrueRouteLength()
327362

328363
// We have to generate a filler that matches all but the last hop (the
@@ -332,18 +367,20 @@ func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash25
332367

333368
for i := 0; i < numHops-1; i++ {
334369
// Sum up how many frames were used by prior hops.
335-
fillerStart := routingInfoSize
370+
fillerStart := routingInfoLen
336371
for _, p := range path[:i] {
337372
fillerStart -= p.HopPayload.NumBytes()
338373
}
339374

340375
// The filler is the part dangling off of the end of the
341376
// routingInfo, so offset it from there, and use the current
342377
// hop's frame count as its size.
343-
fillerEnd := routingInfoSize + path[i].HopPayload.NumBytes()
378+
fillerEnd := routingInfoLen + path[i].HopPayload.NumBytes()
344379

345380
streamKey := generateKey(key, &sharedSecrets[i])
346-
streamBytes := generateCipherStream(streamKey, numStreamBytes)
381+
streamBytes := generateCipherStream(
382+
streamKey, numStreamBytes(routingInfoLen),
383+
)
347384

348385
xor(filler, filler, streamBytes[fillerStart:fillerEnd])
349386
}
@@ -365,7 +402,7 @@ func (f *OnionPacket) Encode(w io.Writer) error {
365402
return err
366403
}
367404

368-
if _, err := w.Write(f.RoutingInfo[:]); err != nil {
405+
if _, err := w.Write(f.RoutingInfo); err != nil {
369406
return err
370407
}
371408

@@ -404,14 +441,24 @@ func (f *OnionPacket) Decode(r io.Reader) error {
404441
return ErrInvalidOnionKey
405442
}
406443

407-
if _, err := io.ReadFull(r, f.RoutingInfo[:]); err != nil {
444+
// To figure out the length of the routing info, we'll read all the
445+
// remaining bytes from the reader.
446+
routingInfoAndMAC, err := io.ReadAll(r)
447+
if err != nil {
408448
return err
409449
}
410450

411-
if _, err := io.ReadFull(r, f.HeaderMAC[:]); err != nil {
412-
return err
451+
// The packet must have at least enough bytes for the HMAC.
452+
if len(routingInfoAndMAC) < HMACSize {
453+
return fmt.Errorf("onion packet is too small, missing HMAC")
413454
}
414455

456+
// With the remainder of the packet read, we can now properly slice the
457+
// routing information and the MAC.
458+
routingInfoLen := len(routingInfoAndMAC) - HMACSize
459+
f.RoutingInfo = routingInfoAndMAC[:routingInfoLen]
460+
copy(f.HeaderMAC[:], routingInfoAndMAC[routingInfoLen:])
461+
415462
return nil
416463
}
417464

@@ -644,11 +691,12 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
644691
dhKey := onionPkt.EphemeralKey
645692
routeInfo := onionPkt.RoutingInfo
646693
headerMac := onionPkt.HeaderMAC
694+
routingInfoLen := len(routeInfo)
647695

648696
// Using the derived shared secret, ensure the integrity of the routing
649697
// information by checking the attached MAC without leaking timing
650698
// information.
651-
message := append(routeInfo[:], assocData...)
699+
message := append(routeInfo, assocData...)
652700
calculatedMac := calcMac(generateKey("mu", sharedSecret), message)
653701
if !hmac.Equal(headerMac[:], calculatedMac[:]) {
654702
return nil, nil, ErrInvalidOnionHMAC
@@ -658,13 +706,14 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
658706
// layer off the routing info revealing the routing information for the
659707
// next hop.
660708
streamBytes := generateCipherStream(
661-
generateKey("rho", sharedSecret), numStreamBytes,
709+
generateKey("rho", sharedSecret),
710+
numStreamBytes(routingInfoLen),
662711
)
663-
zeroBytes := bytes.Repeat([]byte{0}, MaxPayloadSize)
664-
headerWithPadding := append(routeInfo[:], zeroBytes...)
712+
zeroBytes := bytes.Repeat([]byte{0}, routingInfoLen)
713+
headerWithPadding := append(routeInfo, zeroBytes...)
665714

666-
var hopInfo [numStreamBytes]byte
667-
xor(hopInfo[:], headerWithPadding, streamBytes)
715+
hopInfo := make([]byte, numStreamBytes(routingInfoLen))
716+
xor(hopInfo, headerWithPadding, streamBytes)
668717

669718
// Randomize the DH group element for the next hop using the
670719
// deterministic blinding factor.
@@ -682,15 +731,15 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
682731
// payload is treated as such.
683732
hopPayload.Type = PayloadTLV
684733
}
685-
err := hopPayload.Decode(bytes.NewReader(hopInfo[:]))
734+
err := hopPayload.Decode(bytes.NewReader(hopInfo))
686735
if err != nil {
687736
return nil, nil, err
688737
}
689738

690739
// With the necessary items extracted, we'll copy of the onion packet
691740
// for the next node, snipping off our per-hop data.
692-
var nextMixHeader [routingInfoSize]byte
693-
copy(nextMixHeader[:], hopInfo[hopPayload.NumBytes():])
741+
var nextMixHeader = make([]byte, routingInfoLen)
742+
copy(nextMixHeader, hopInfo[hopPayload.NumBytes():])
694743
innerPkt := &OnionPacket{
695744
Version: onionPkt.Version,
696745
EphemeralKey: nextDHKey,
@@ -853,3 +902,12 @@ func (t *Tx) Commit() ([]ProcessedPacket, *ReplaySet, error) {
853902

854903
return t.packets, rs, err
855904
}
905+
906+
// numStreamBytes is the number of bytes that needs to be produced by our CSPRG
907+
// for the key stream implementing our stream cipher to encrypt/decrypt the mix
908+
// header. The routingInfoSize bytes at the end are used to encrypt/decrypt the
909+
// fillers when processing the packet of generating the HMACs when creating the
910+
// packet.
911+
func numStreamBytes(routingInfoSize int) uint {
912+
return uint(routingInfoSize * 2)
913+
}

0 commit comments

Comments
 (0)