Skip to content

Commit dad655d

Browse files
committed
fixup! multi: Support custom size onion packets
1 parent 2ee72d4 commit dad655d

File tree

5 files changed

+222
-158
lines changed

5 files changed

+222
-158
lines changed

cmd/main.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +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.",
73+
cli.IntFlag{
74+
Name: "payload-size",
75+
Usage: "The size for a payload for a " +
76+
"single hop. Defaults to the " +
77+
"max routing payload size",
78+
Value: sphinx.MaxRoutingPayloadSize,
7879
},
7980
},
8081
},
@@ -209,18 +210,10 @@ func generate(ctx *cli.Context) error {
209210
return fmt.Errorf("could not peel onion spec: %v", err)
210211
}
211212

212-
payloadSizes := []int{
213-
sphinx.MaxRoutingPayloadSize,
214-
}
215-
if ctx.Bool("onion-message") {
216-
payloadSizes = append(
217-
payloadSizes,
218-
sphinx.MaxOnionMessagePayloadSize,
219-
)
220-
}
213+
payloadSize := ctx.Int("payload-size")
221214
msg, err := sphinx.NewOnionPacket(
222215
path, sessionKey, assocData, sphinx.DeterministicPacketFiller,
223-
payloadSizes...,
216+
sphinx.WithMaxPayloadSize(payloadSize),
224217
)
225218
if err != nil {
226219
return fmt.Errorf("error creating message: %v", err)

error.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package sphinx
22

3-
import "fmt"
3+
import (
4+
"errors"
5+
"fmt"
6+
)
47

58
var (
69
// ErrReplayedPacket is an error returned when a packet is rejected
@@ -21,7 +24,36 @@ var (
2124
ErrInvalidOnionKey = fmt.Errorf("invalid onion key: pubkey isn't on " +
2225
"secp256k1 curve")
2326

24-
// ErrLogEntryNotFound is an error returned when a packet lookup in a replay
25-
// log fails because it is missing.
26-
ErrLogEntryNotFound = fmt.Errorf("sphinx packet is not in log")
27+
// ErrLogEntryNotFound is an error returned when a packet lookup in a
28+
// replay log fails because it is missing.
29+
ErrLogEntryNotFound = errors.New("sphinx packet is not in log")
30+
31+
// ErrPayloadSizeExceeded is returned when the payload size exceeds the
32+
// configured payload size of the onion packet.
33+
ErrPayloadSizeExceeded = errors.New("max payload size exceeded")
34+
35+
// ErrSharedSecretDerivation is returned when we fail to derive the
36+
// shared secret for a hop.
37+
ErrSharedSecretDerivation = errors.New("error generating shared secret")
38+
39+
// ErrMissingHMAC is returned when the onion packet is too small to
40+
// contain a valid HMAC.
41+
ErrMissingHMAC = errors.New("onion packet is too small, missing HMAC")
42+
43+
// ErrNegativeRoutingInfoSize is returned when a negative routing info
44+
// size is specified in the Sphinx configuration.
45+
ErrNegativeRoutingInfoSize = errors.New("payload size must be " +
46+
"non-negative")
47+
48+
// ErrNegativePayloadSize is returned when a negative payload size is
49+
// specified in the Sphinx configuration.
50+
ErrNegativePayloadSize = errors.New("payload size must be " +
51+
"non-negative")
52+
53+
// ErrZeroHops is returned when attempting to create a route with zero
54+
// hops.
55+
ErrZeroHops = errors.New("route of length zero passed in")
56+
57+
// ErrIOReadFull is returned when an io read full operation fails.
58+
ErrIOReadFull = errors.New("io read full error")
2759
)

packetfiller.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ func RandPacketFiller(_ *btcec.PrivateKey, mixHeader []byte) error {
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+
_, err := rand.Read(mixHeader)
25+
if err != nil {
2526
return err
2627
}
2728

@@ -55,6 +56,7 @@ func DeterministicPacketFiller(sessionKey *btcec.PrivateKey,
5556
if err != nil {
5657
return err
5758
}
59+
5860
padCipher.XORKeyStream(mixHeader, mixHeader)
5961

6062
return nil

sphinx.go

Lines changed: 82 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"crypto/sha256"
77
"fmt"
88
"io"
9-
"sort"
9+
"slices"
1010
"sync"
1111

1212
"github.com/btcsuite/btcd/btcec/v2"
@@ -60,10 +60,11 @@ const (
6060

6161
// baseVersion represent the current supported version of onion packet.
6262
baseVersion = 0
63-
)
6463

65-
var (
66-
ErrPayloadSizeExceeded = fmt.Errorf("max custom payload size exceeded")
64+
// streamBytesMultiplier is the multiplier used to calculate the number
65+
// of bytes that needs to be produced by our CSPRNG for the key stream
66+
// implementing our stream cipher.
67+
streamBytesMultiplier = 2
6768
)
6869

6970
// OnionPacket is the onion wrapped hop-to-hop routing information necessary to
@@ -180,66 +181,59 @@ func generateSharedSecrets(paymentPath []*btcec.PublicKey,
180181
return hopSharedSecrets, lastEphemeralPubKey, nil
181182
}
182183

184+
type newOnionPacketCfg struct {
185+
payloadSize int
186+
}
187+
188+
// NewOnionPacketOpt defines the signature of a function option that can be used
189+
// with NewOnionPacket.
190+
type NewOnionPacketOpt func(cfg *newOnionPacketCfg)
191+
192+
// WithMaxPayloadSize is a function option that can be used to set the maximum
193+
// payload size.
194+
func WithMaxPayloadSize(size int) NewOnionPacketOpt {
195+
return func(cfg *newOnionPacketCfg) {
196+
cfg.payloadSize = size
197+
}
198+
}
199+
183200
// NewOnionPacket creates a new onion packet which is capable of obliviously
184201
// routing a message through the mix-net path outline by 'paymentPath'. The
185202
// total size of the onion 'clicks' to the first value in payloadSizes that is
186203
// bigger than the total payload size of the path. If no size is given, it
187204
// defaults to the maximum routing payload size.
188205
func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
189206
assocData []byte, pktFiller PacketFiller,
190-
payloadSizes ...int) (*OnionPacket, error) {
207+
opts ...NewOnionPacketOpt) (*OnionPacket, error) {
191208

192-
// If we don't actually have a partially populated route, then we'll
193-
// exit early.
194-
numHops := paymentPath.TrueRouteLength()
195-
if numHops == 0 {
196-
return nil, fmt.Errorf("route of length zero passed in")
209+
cfg := &newOnionPacketCfg{}
210+
for _, o := range opts {
211+
o(cfg)
197212
}
198213

199-
totalPayloadSize := paymentPath.TotalPayloadSize()
214+
if cfg.payloadSize < 0 {
215+
return nil, ErrNegativePayloadSize
216+
}
200217

201218
// We default to the maximum routing payload size if the caller didn't
202219
// provide any payload sizes.
203-
if len(payloadSizes) == 0 {
204-
payloadSizes = []int{MaxRoutingPayloadSize}
220+
if cfg.payloadSize == 0 {
221+
cfg.payloadSize = MaxRoutingPayloadSize
205222
}
206223

207-
sort.Ints(payloadSizes)
208-
209-
// We'll now select the smallest payload size that is large enough to
210-
// fit the entire onion payload. If no such size exists, then we'll
211-
// return an error
212-
var payloadSize int
213-
found := false
214-
for _, size := range payloadSizes {
215-
if size >= totalPayloadSize {
216-
payloadSize = size
217-
found = true
218-
break
219-
}
224+
// If we don't actually have a partially populated route, then we'll
225+
// exit early.
226+
numHops := paymentPath.TrueRouteLength()
227+
if numHops == 0 {
228+
return nil, fmt.Errorf("route of length zero passed in")
220229
}
221230

222-
// Return an error if we couldn't find a suitable payload size.
223-
if !found {
224-
return nil, ErrPayloadSizeExceeded
225-
}
231+
totalPayloadSize := paymentPath.TotalPayloadSize()
226232

227-
// If the payload size is not equal to MaxRoutingPayloadSize, then we
228-
// check if any of the hops have a legacy payload. If so, we return an
229-
// error as legacy payloads are not supported for those payload sizes.
230-
if payloadSize != MaxRoutingPayloadSize {
231-
for i := 0; i < numHops; i++ {
232-
hopPayload := (*paymentPath)[i].HopPayload
233-
isLegacy := hopPayload.Type == PayloadLegacy
234-
235-
// For any onion size other than MaxRoutingPayloadSize,
236-
// we only expect TLV payloads.
237-
if isLegacy {
238-
return nil, fmt.Errorf("hop %d has legacy "+
239-
"payload, but this payload size "+
240-
"requires TLV,", i)
241-
}
242-
}
233+
// Return an error if the actual payload size exceeds the configured
234+
// payload size.
235+
if totalPayloadSize > cfg.payloadSize {
236+
return nil, ErrPayloadSizeExceeded
243237
}
244238

245239
// We'll force the caller to provide a packet filler, as otherwise we
@@ -253,25 +247,28 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
253247
paymentPath.NodeKeys(), sessionKey,
254248
)
255249
if err != nil {
256-
return nil, fmt.Errorf("error generating shared secret: %v",
257-
err)
250+
return nil, fmt.Errorf("%w: %w", ErrSharedSecretDerivation, err)
258251
}
259252

260253
// Generate the padding, called "filler strings" in the paper.
261-
filler := generateHeaderPadding(
262-
"rho", paymentPath, hopSharedSecrets, payloadSize,
254+
filler, err := generateHeaderPadding(
255+
"rho", paymentPath, hopSharedSecrets, cfg.payloadSize,
263256
)
257+
if err != nil {
258+
return nil, err
259+
}
264260

265261
// Allocate zero'd out byte slices to store the final mix header packet
266262
// and the hmac for each hop.
267263
var (
268-
mixHeader = make([]byte, payloadSize)
264+
mixHeader = make([]byte, cfg.payloadSize)
269265
nextHmac [HMACSize]byte
270266
hopPayloadBuf bytes.Buffer
271267
)
272268

273269
// Fill the packet using the caller specified methodology.
274-
if err := pktFiller(sessionKey, mixHeader); err != nil {
270+
err = pktFiller(sessionKey, mixHeader)
271+
if err != nil {
275272
return nil, err
276273
}
277274

@@ -292,7 +289,9 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
292289
// Next, using the key dedicated for our stream cipher, we'll
293290
// generate enough bytes to obfuscate this layer of the onion
294291
// packet.
295-
streamBytes := generateCipherStream(rhoKey, uint(payloadSize))
292+
streamBytes := generateCipherStream(
293+
rhoKey, uint(cfg.payloadSize),
294+
)
296295
payload := paymentPath[i].HopPayload
297296

298297
// Before we assemble the packet, we'll shift the current
@@ -323,7 +322,7 @@ func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
323322
// calculating the MAC, we'll also include the optional
324323
// associated data which can allow higher level applications to
325324
// prevent replay attacks.
326-
packet := append(mixHeader, assocData...)
325+
packet := slices.Concat(mixHeader, assocData)
327326
nextHmac = calcMac(muKey, packet)
328327

329328
hopPayloadBuf.Reset()
@@ -361,7 +360,7 @@ func rightShift(slice []byte, num int) {
361360
// last hop. Using this methodology, the size of the field stays constant at
362361
// each hop.
363362
func generateHeaderPadding(key string, path *PaymentPath,
364-
sharedSecrets []Hash256, routingInfoLen int) []byte {
363+
sharedSecrets []Hash256, routingInfoLen int) ([]byte, error) {
365364

366365
numHops := path.TrueRouteLength()
367366

@@ -383,14 +382,18 @@ func generateHeaderPadding(key string, path *PaymentPath,
383382
fillerEnd := routingInfoLen + path[i].HopPayload.NumBytes()
384383

385384
streamKey := generateKey(key, &sharedSecrets[i])
386-
streamBytes := generateCipherStream(
387-
streamKey, numStreamBytes(routingInfoLen),
388-
)
385+
386+
streamBytesLen, err := numStreamBytes(routingInfoLen)
387+
if err != nil {
388+
return nil, err
389+
}
390+
391+
streamBytes := generateCipherStream(streamKey, streamBytesLen)
389392

390393
xor(filler, filler, streamBytes[fillerStart:fillerEnd])
391394
}
392395

393-
return filler
396+
return filler, nil
394397
}
395398

396399
// Encode serializes the raw bytes of the onion packet into the passed
@@ -407,7 +410,8 @@ func (f *OnionPacket) Encode(w io.Writer) error {
407410
return err
408411
}
409412

410-
if _, err := w.Write(f.RoutingInfo); err != nil {
413+
_, err = w.Write(f.RoutingInfo)
414+
if err != nil {
411415
return err
412416
}
413417

@@ -455,7 +459,7 @@ func (f *OnionPacket) Decode(r io.Reader) error {
455459

456460
// The packet must have at least enough bytes for the HMAC.
457461
if len(routingInfoAndMAC) < HMACSize {
458-
return fmt.Errorf("onion packet is too small, missing HMAC")
462+
return ErrMissingHMAC
459463
}
460464

461465
// With the remainder of the packet read, we can now properly slice the
@@ -701,7 +705,7 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
701705
// Using the derived shared secret, ensure the integrity of the routing
702706
// information by checking the attached MAC without leaking timing
703707
// information.
704-
message := append(routeInfo, assocData...)
708+
message := slices.Concat(routeInfo, assocData)
705709
calculatedMac := calcMac(generateKey("mu", sharedSecret), message)
706710
if !hmac.Equal(headerMac[:], calculatedMac[:]) {
707711
return nil, nil, ErrInvalidOnionHMAC
@@ -710,14 +714,17 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
710714
// Attach the padding zeroes in order to properly strip an encryption
711715
// layer off the routing info revealing the routing information for the
712716
// next hop.
717+
streamBytesLen, err := numStreamBytes(routingInfoLen)
718+
if err != nil {
719+
return nil, nil, err
720+
}
713721
streamBytes := generateCipherStream(
714-
generateKey("rho", sharedSecret),
715-
numStreamBytes(routingInfoLen),
722+
generateKey("rho", sharedSecret), streamBytesLen,
716723
)
717724
zeroBytes := bytes.Repeat([]byte{0}, routingInfoLen)
718-
headerWithPadding := append(routeInfo, zeroBytes...)
725+
headerWithPadding := slices.Concat(routeInfo, zeroBytes)
719726

720-
hopInfo := make([]byte, numStreamBytes(routingInfoLen))
727+
hopInfo := make([]byte, streamBytesLen)
721728
xor(hopInfo, headerWithPadding, streamBytes)
722729

723730
// Randomize the DH group element for the next hop using the
@@ -736,7 +743,8 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
736743
// payload is treated as such.
737744
hopPayload.Type = PayloadTLV
738745
}
739-
err := hopPayload.Decode(bytes.NewReader(hopInfo))
746+
747+
err = hopPayload.Decode(bytes.NewReader(hopInfo))
740748
if err != nil {
741749
return nil, nil, err
742750
}
@@ -908,11 +916,15 @@ func (t *Tx) Commit() ([]ProcessedPacket, *ReplaySet, error) {
908916
return t.packets, rs, err
909917
}
910918

911-
// numStreamBytes is the number of bytes that needs to be produced by our CSPRG
919+
// numStreamBytes is the number of bytes that needs to be produced by our CSPRNG
912920
// for the key stream implementing our stream cipher to encrypt/decrypt the mix
913921
// header. The routingInfoSize bytes at the end are used to encrypt/decrypt the
914922
// fillers when processing the packet of generating the HMACs when creating the
915923
// packet.
916-
func numStreamBytes(routingInfoSize int) uint {
917-
return uint(routingInfoSize * 2)
924+
func numStreamBytes(routingInfoSize int) (uint, error) {
925+
if routingInfoSize < 0 {
926+
return 0, ErrNegativeRoutingInfoSize
927+
}
928+
929+
return uint(routingInfoSize) * streamBytesMultiplier, nil
918930
}

0 commit comments

Comments
 (0)