Skip to content

Commit e9cf493

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.
1 parent 94d1a0f commit e9cf493

File tree

4 files changed

+311
-58
lines changed

4 files changed

+311
-58
lines changed

cmd/main.go

Lines changed: 12 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
{
@@ -203,8 +209,14 @@ func generate(ctx *cli.Context) error {
203209
return fmt.Errorf("could not peel onion spec: %v", err)
204210
}
205211

212+
var onionOpts []sphinx.OnionPacketOption
213+
if ctx.Bool("onion-message") {
214+
onionOpts = append(onionOpts, sphinx.WithOnionMessage())
215+
}
216+
206217
msg, err := sphinx.NewOnionPacket(
207218
path, sessionKey, assocData, sphinx.DeterministicPacketFiller,
219+
onionOpts...,
208220
)
209221
if err != nil {
210222
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: 113 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,13 @@ 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
53-
// each hop of the route, the first pair in cleartext and the following
54-
// pairs increasingly obfuscated. If not all space is used up, the
55-
// remainder is padded with null-bytes, also obfuscated.
56-
routingInfoSize = 1300
57-
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
44+
// StandardRoutingInfoSize is the size of the routing info for a standard
45+
// onion packet.
46+
StandardRoutingInfoSize = 1300
47+
48+
// JumboRoutingInfoSize is the size of the routing info for a jumbo
49+
// onion packet.
50+
JumboRoutingInfoSize = 32768
6451

6552
// keyLen is the length of the keys used to generate cipher streams and
6653
// encrypt payloads. Since we use SHA256 to generate the keys, the
@@ -72,8 +59,15 @@ const (
7259
)
7360

7461
var (
75-
ErrMaxRoutingInfoSizeExceeded = fmt.Errorf(
76-
"max routing info size of %v bytes exceeded", routingInfoSize)
62+
ErrStandardRoutingInfoSizeExceeded = fmt.Errorf(
63+
"max routing info size of %v bytes exceeded",
64+
StandardRoutingInfoSize,
65+
)
66+
67+
ErrJumboRoutingInfoSizeExceeded = fmt.Errorf(
68+
"max onion message routing info size of %v bytes exceeded",
69+
JumboRoutingInfoSize,
70+
)
7771
)
7872

7973
// OnionPacket is the onion wrapped hop-to-hop routing information necessary to
@@ -102,7 +96,7 @@ type OnionPacket struct {
10296
// RoutingInfo is the full routing information for this onion packet.
10397
// This encodes all the forwarding instructions for this current hop
10498
// and all the hops in the route.
105-
RoutingInfo [routingInfoSize]byte
99+
RoutingInfo []byte
106100

107101
// HeaderMAC is an HMAC computed with the shared secret of the routing
108102
// data and the associated data for this route. Including the
@@ -190,14 +184,54 @@ func generateSharedSecrets(paymentPath []*btcec.PublicKey,
190184
return hopSharedSecrets, lastEphemeralPubKey, nil
191185
}
192186

187+
// onionPacketCfg is a struct that holds the configuration for creating a new
188+
// onion packet.
189+
type onionPacketCfg struct {
190+
isOnionMessage bool
191+
}
192+
193+
// OnionPacketOption is a function that can be used to modify the onion packet
194+
// configuration.
195+
type OnionPacketOption func(*onionPacketCfg)
196+
197+
// WithOnionMessage is a functional option that signals that the onion packet
198+
// being created is an onion message.
199+
func WithOnionMessage() OnionPacketOption {
200+
return func(cfg *onionPacketCfg) {
201+
cfg.isOnionMessage = true
202+
}
203+
}
204+
193205
// NewOnionPacket creates a new onion packet which is capable of obliviously
194206
// routing a message through the mix-net path outline by 'paymentPath'.
195207
func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
196-
assocData []byte, pktFiller PacketFiller) (*OnionPacket, error) {
208+
assocData []byte, pktFiller PacketFiller,
209+
opts ...OnionPacketOption) (*OnionPacket, error) {
210+
211+
cfg := &onionPacketCfg{}
212+
for _, o := range opts {
213+
o(cfg)
214+
}
215+
216+
totalPayloadSize := paymentPath.TotalPayloadSize()
217+
218+
var routingInfoLen int
219+
maxRoutingInfoErr := ErrStandardRoutingInfoSizeExceeded
220+
if cfg.isOnionMessage {
221+
switch {
222+
case totalPayloadSize <= StandardRoutingInfoSize:
223+
routingInfoLen = StandardRoutingInfoSize
224+
default:
225+
routingInfoLen = JumboRoutingInfoSize
226+
maxRoutingInfoErr = ErrJumboRoutingInfoSizeExceeded
227+
}
228+
} else {
229+
routingInfoLen = StandardRoutingInfoSize
230+
}
197231

198232
// Check whether total payload size doesn't exceed the hard maximum.
199-
if paymentPath.TotalPayloadSize() > routingInfoSize {
200-
return nil, ErrMaxRoutingInfoSizeExceeded
233+
if totalPayloadSize > routingInfoLen {
234+
return nil, maxRoutingInfoErr
201235
}
202236

203237
// If we don't actually have a partially populated route, then we'll
@@ -207,6 +241,20 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
207241
return nil, fmt.Errorf("route of length zero passed in")
208242
}
209243

244+
// Before we proceed, we'll check that the payload types of each hop
245+
// in the payment path match the type of onion packet we're creating.
246+
for i := 0; i < numHops; i++ {
247+
hopPayload := (*paymentPath)[i].HopPayload
248+
isLegacy := hopPayload.Type == PayloadLegacy
249+
250+
// If this is an onion message, we only expect TLV
251+
// payloads.
252+
if cfg.isOnionMessage && isLegacy {
253+
return nil, fmt.Errorf("hop %d has legacy payload, "+
254+
"but onion messages require TLV", i)
255+
}
256+
}
257+
210258
// We'll force the caller to provide a packet filler, as otherwise we
211259
// may default to an insecure filling method (which should only really
212260
// be used to generate test vectors).
@@ -222,18 +270,20 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
222270
}
223271

224272
// Generate the padding, called "filler strings" in the paper.
225-
filler := generateHeaderPadding("rho", paymentPath, hopSharedSecrets)
273+
filler := generateHeaderPadding(
274+
"rho", paymentPath, hopSharedSecrets, routingInfoLen,
275+
)
226276

227277
// Allocate zero'd out byte slices to store the final mix header packet
228278
// and the hmac for each hop.
229279
var (
230-
mixHeader [routingInfoSize]byte
280+
mixHeader = make([]byte, routingInfoLen)
231281
nextHmac [HMACSize]byte
232282
hopPayloadBuf bytes.Buffer
233283
)
234284

235285
// Fill the packet using the caller specified methodology.
236-
if err := pktFiller(sessionKey, &mixHeader); err != nil {
286+
if err := pktFiller(sessionKey, mixHeader); err != nil {
237287
return nil, err
238288
}
239289

@@ -254,26 +304,26 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
254304
// Next, using the key dedicated for our stream cipher, we'll
255305
// generate enough bytes to obfuscate this layer of the onion
256306
// packet.
257-
streamBytes := generateCipherStream(rhoKey, routingInfoSize)
307+
streamBytes := generateCipherStream(rhoKey, uint(routingInfoLen))
258308
payload := paymentPath[i].HopPayload
259309

260310
// Before we assemble the packet, we'll shift the current
261311
// mix-header to the right in order to make room for this next
262312
// per-hop data.
263313
shiftSize := payload.NumBytes()
264-
rightShift(mixHeader[:], shiftSize)
314+
rightShift(mixHeader, shiftSize)
265315

266316
err := payload.Encode(&hopPayloadBuf)
267317
if err != nil {
268318
return nil, err
269319
}
270320

271-
copy(mixHeader[:], hopPayloadBuf.Bytes())
321+
copy(mixHeader, hopPayloadBuf.Bytes())
272322

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

278328
// If this is the "last" hop, then we'll override the tail of
279329
// the hop data.
@@ -285,7 +335,7 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
285335
// calculating the MAC, we'll also include the optional
286336
// associated data which can allow higher level applications to
287337
// prevent replay attacks.
288-
packet := append(mixHeader[:], assocData...)
338+
packet := append(mixHeader, assocData...)
289339
nextHmac = calcMac(muKey, packet)
290340

291341
hopPayloadBuf.Reset()
@@ -322,7 +372,8 @@ func rightShift(slice []byte, num int) {
322372
// leaving only the original "filler" bytes produced by this function at the
323373
// last hop. Using this methodology, the size of the field stays constant at
324374
// each hop.
325-
func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash256) []byte {
375+
func generateHeaderPadding(key string, path *PaymentPath,
376+
sharedSecrets []Hash256, routingInfoLen int) []byte {
326377
numHops := path.TrueRouteLength()
327378

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

333384
for i := 0; i < numHops-1; i++ {
334385
// Sum up how many frames were used by prior hops.
335-
fillerStart := routingInfoSize
386+
fillerStart := routingInfoLen
336387
for _, p := range path[:i] {
337388
fillerStart -= p.HopPayload.NumBytes()
338389
}
339390

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

345396
streamKey := generateKey(key, &sharedSecrets[i])
346-
streamBytes := generateCipherStream(streamKey, numStreamBytes)
397+
streamBytes := generateCipherStream(streamKey, uint(routingInfoLen*2))
347398

348399
xor(filler, filler, streamBytes[fillerStart:fillerEnd])
349400
}
@@ -365,7 +416,7 @@ func (f *OnionPacket) Encode(w io.Writer) error {
365416
return err
366417
}
367418

368-
if _, err := w.Write(f.RoutingInfo[:]); err != nil {
419+
if _, err := w.Write(f.RoutingInfo); err != nil {
369420
return err
370421
}
371422

@@ -404,14 +455,24 @@ func (f *OnionPacket) Decode(r io.Reader) error {
404455
return ErrInvalidOnionKey
405456
}
406457

407-
if _, err := io.ReadFull(r, f.RoutingInfo[:]); err != nil {
458+
// To figure out the length of the routing info, we'll read all the
459+
// remaining bytes from the reader.
460+
routingInfoAndMAC, err := io.ReadAll(r)
461+
if err != nil {
408462
return err
409463
}
410464

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

470+
// With the remainder of the packet read, we can now properly slice the
471+
// routing information and the MAC.
472+
routingInfoLen := len(routingInfoAndMAC) - HMACSize
473+
f.RoutingInfo = routingInfoAndMAC[:routingInfoLen]
474+
copy(f.HeaderMAC[:], routingInfoAndMAC[routingInfoLen:])
475+
415476
return nil
416477
}
417478

@@ -644,11 +705,12 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
644705
dhKey := onionPkt.EphemeralKey
645706
routeInfo := onionPkt.RoutingInfo
646707
headerMac := onionPkt.HeaderMAC
708+
routingInfoLen := len(routeInfo)
647709

648710
// Using the derived shared secret, ensure the integrity of the routing
649711
// information by checking the attached MAC without leaking timing
650712
// information.
651-
message := append(routeInfo[:], assocData...)
713+
message := append(routeInfo, assocData...)
652714
calculatedMac := calcMac(generateKey("mu", sharedSecret), message)
653715
if !hmac.Equal(headerMac[:], calculatedMac[:]) {
654716
return nil, nil, ErrInvalidOnionHMAC
@@ -658,13 +720,13 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
658720
// layer off the routing info revealing the routing information for the
659721
// next hop.
660722
streamBytes := generateCipherStream(
661-
generateKey("rho", sharedSecret), numStreamBytes,
723+
generateKey("rho", sharedSecret), uint(routingInfoLen*2),
662724
)
663-
zeroBytes := bytes.Repeat([]byte{0}, MaxPayloadSize)
664-
headerWithPadding := append(routeInfo[:], zeroBytes...)
725+
zeroBytes := bytes.Repeat([]byte{0}, routingInfoLen)
726+
headerWithPadding := append(routeInfo, zeroBytes...)
665727

666-
var hopInfo [numStreamBytes]byte
667-
xor(hopInfo[:], headerWithPadding, streamBytes)
728+
hopInfo := make([]byte, routingInfoLen*2)
729+
xor(hopInfo, headerWithPadding, streamBytes)
668730

669731
// Randomize the DH group element for the next hop using the
670732
// deterministic blinding factor.
@@ -675,15 +737,15 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
675737
// out the payload so we can derive the specified forwarding
676738
// instructions.
677739
var hopPayload HopPayload
678-
err := hopPayload.Decode(bytes.NewReader(hopInfo[:]), isOnionMessage)
740+
err := hopPayload.Decode(bytes.NewReader(hopInfo), isOnionMessage)
679741
if err != nil {
680742
return nil, nil, err
681743
}
682744

683745
// With the necessary items extracted, we'll copy of the onion packet
684746
// for the next node, snipping off our per-hop data.
685-
var nextMixHeader [routingInfoSize]byte
686-
copy(nextMixHeader[:], hopInfo[hopPayload.NumBytes():])
747+
var nextMixHeader = make([]byte, routingInfoLen)
748+
copy(nextMixHeader, hopInfo[hopPayload.NumBytes():])
687749
innerPkt := &OnionPacket{
688750
Version: onionPkt.Version,
689751
EphemeralKey: nextDHKey,

0 commit comments

Comments
 (0)