-
Notifications
You must be signed in to change notification settings - Fork 127
/
sphinx.go
831 lines (698 loc) · 28.6 KB
/
sphinx.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
package sphinx
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"fmt"
"io"
"sync"
"github.com/btcsuite/btcd/btcec/v2"
)
const (
// addressSize is the length of the serialized address used to uniquely
// identify the next hop to forward the onion to. BOLT 04 defines this
// as 8 byte channel_id.
AddressSize = 8
// RealmByteSize is the number of bytes that the realm byte occupies.
RealmByteSize = 1
// AmtForwardSize is the number of bytes that the amount to forward
// occupies.
AmtForwardSize = 8
// OutgoingCLTVSize is the number of bytes that the outgoing CLTV value
// occupies.
OutgoingCLTVSize = 4
// NumPaddingBytes is the number of padding bytes in the hopData. These
// bytes are currently unused within the protocol, and are reserved for
// future use. However, if a hop contains extra data, then we'll
// utilize this space to pack in the unrolled bytes.
NumPaddingBytes = 12
// LegacyHopDataSize is the fixed size of hop_data. BOLT 04 currently
// specifies this to be 1 byte realm, 8 byte channel_id, 8 byte amount
// to forward, 4 byte outgoing CLTV value, 12 bytes padding and 32 bytes
// HMAC for a total of 65 bytes per hop.
LegacyHopDataSize = (RealmByteSize + AddressSize + AmtForwardSize +
OutgoingCLTVSize + NumPaddingBytes + HMACSize)
// MaxPayloadSize is the maximum size a payload for a single hop can be.
// This is the worst case scenario of a single hop, consuming all
// available space. We need to know this in order to generate a
// sufficiently long stream of pseudo-random bytes when
// encrypting/decrypting the payload.
MaxPayloadSize = routingInfoSize
// routingInfoSize is the fixed size of the the routing info. This
// consists of a addressSize byte address and a HMACSize byte HMAC for
// each hop of the route, the first pair in cleartext and the following
// pairs increasingly obfuscated. If not all space is used up, the
// remainder is padded with null-bytes, also obfuscated.
routingInfoSize = 1300
// numStreamBytes is the number of bytes produced by our CSPRG for the
// key stream implementing our stream cipher to encrypt/decrypt the mix
// header. The MaxPayloadSize bytes at the end are used to
// encrypt/decrypt the fillers when processing the packet of generating
// the HMACs when creating the packet.
numStreamBytes = routingInfoSize * 2
// keyLen is the length of the keys used to generate cipher streams and
// encrypt payloads. Since we use SHA256 to generate the keys, the
// maximum length currently is 32 bytes.
keyLen = 32
// baseVersion represent the current supported version of onion packet.
baseVersion = 0
)
var (
ErrMaxRoutingInfoSizeExceeded = fmt.Errorf(
"max routing info size of %v bytes exceeded", routingInfoSize)
)
// OnionPacket is the onion wrapped hop-to-hop routing information necessary to
// propagate a message through the mix-net without intermediate nodes having
// knowledge of their position within the route, the source, the destination,
// and finally the identities of the past/future nodes in the route. At each
// hop the ephemeral key is used by the node to perform ECDH between itself and
// the source node. This derived secret key is used to check the MAC of the
// entire mix header, decrypt the next set of routing information, and
// re-randomize the ephemeral key for the next node in the path. This per-hop
// re-randomization allows us to only propagate a single group element through
// the onion route.
type OnionPacket struct {
// Version denotes the version of this onion packet. The version
// indicates how a receiver of the packet should interpret the bytes
// following this version byte. Currently, a version of 0x00 is the
// only defined version type.
Version byte
// EphemeralKey is the public key that each hop will used in
// combination with the private key in an ECDH to derive the shared
// secret used to check the HMAC on the packet and also decrypted the
// routing information.
EphemeralKey *btcec.PublicKey
// RoutingInfo is the full routing information for this onion packet.
// This encodes all the forwarding instructions for this current hop
// and all the hops in the route.
RoutingInfo [routingInfoSize]byte
// HeaderMAC is an HMAC computed with the shared secret of the routing
// data and the associated data for this route. Including the
// associated data lets each hop authenticate higher-level data that is
// critical for the forwarding of this HTLC.
HeaderMAC [HMACSize]byte
}
// generateSharedSecrets by the given nodes pubkeys, generates the shared
// secrets.
func generateSharedSecrets(paymentPath []*btcec.PublicKey,
sessionKey *btcec.PrivateKey) ([]Hash256, *btcec.PublicKey, error) {
// Each hop performs ECDH with our ephemeral key pair to arrive at a
// shared secret. Additionally, each hop randomizes the group element
// for the next hop by multiplying it by the blinding factor. This way
// we only need to transmit a single group element, and hops can't link
// a session back to us if they have several nodes in the path.
numHops := len(paymentPath)
hopSharedSecrets := make([]Hash256, numHops)
// Compute the triplet for the first hop outside of the main loop.
// Within the loop each new triplet will be computed recursively based
// off of the blinding factor of the last hop.
lastEphemeralPubKey := sessionKey.PubKey()
sessionKeyECDH := &PrivKeyECDH{PrivKey: sessionKey}
sharedSecret, err := sessionKeyECDH.ECDH(paymentPath[0])
if err != nil {
return nil, nil, err
}
hopSharedSecrets[0] = sharedSecret
lastBlindingFactor := computeBlindingFactor(
lastEphemeralPubKey, hopSharedSecrets[0][:],
)
// The cached blinding factor will contain the running product of the
// session private key x and blinding factors b_i, computed as
// c_0 = x
// c_i = c_{i-1} * b_{i-1} (mod |F(G)|).
// = x * b_0 * b_1 * ... * b_{i-1} (mod |F(G)|).
//
// We begin with just the session private key x, so that base case
// c_0 = x. At the beginning of each iteration, the previous blinding
// factor is aggregated into the modular product, and used as the scalar
// value in deriving the hop ephemeral keys and shared secrets.
var cachedBlindingFactor btcec.ModNScalar
cachedBlindingFactor.Set(&sessionKey.Key)
// Now recursively compute the cached blinding factor, ephemeral ECDH
// pub keys, and shared secret for each hop.
var nextBlindingFactor btcec.ModNScalar
for i := 1; i <= numHops-1; i++ {
// Update the cached blinding factor with b_{i-1}.
nextBlindingFactor.Set(&lastBlindingFactor)
cachedBlindingFactor.Mul(&nextBlindingFactor)
// a_i = g ^ c_i
// = g^( x * b_0 * ... * b_{i-1} )
// = X^( b_0 * ... * b_{i-1} )
// X_our_session_pub_key x all prev blinding factors
lastEphemeralPubKey = blindBaseElement(cachedBlindingFactor)
// e_i = Y_i ^ c_i
// = ( Y_i ^ x )^( b_0 * ... * b_{i-1} )
// (Y_their_pub_key x x_our_priv) x all prev blinding factors
hopBlindedPubKey := blindGroupElement(
paymentPath[i], cachedBlindingFactor,
)
// s_i = sha256( e_i )
// = sha256( Y_i ^ (x * b_0 * ... * b_{i-1} )
hopSharedSecrets[i] = sha256.Sum256(hopBlindedPubKey.SerializeCompressed())
// Only need to evaluate up to the penultimate blinding factor.
if i >= numHops-1 {
break
}
// b_i = sha256( a_i || s_i )
lastBlindingFactor = computeBlindingFactor(
lastEphemeralPubKey, hopSharedSecrets[i][:],
)
}
return hopSharedSecrets, lastEphemeralPubKey, nil
}
// NewOnionPacket creates a new onion packet which is capable of obliviously
// routing a message through the mix-net path outline by 'paymentPath'.
func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
assocData []byte, pktFiller PacketFiller) (*OnionPacket, error) {
// Check whether total payload size doesn't exceed the hard maximum.
if paymentPath.TotalPayloadSize() > routingInfoSize {
return nil, ErrMaxRoutingInfoSizeExceeded
}
// If we don't actually have a partially populated route, then we'll
// exit early.
numHops := paymentPath.TrueRouteLength()
if numHops == 0 {
return nil, fmt.Errorf("route of length zero passed in")
}
// We'll force the caller to provide a packet filler, as otherwise we
// may default to an insecure filling method (which should only really
// be used to generate test vectors).
if pktFiller == nil {
return nil, fmt.Errorf("packet filler must be specified")
}
hopSharedSecrets, _, err := generateSharedSecrets(
paymentPath.NodeKeys(), sessionKey,
)
if err != nil {
return nil, fmt.Errorf("error generating shared secret: %v", err)
}
// Generate the padding, called "filler strings" in the paper.
filler := generateHeaderPadding("rho", paymentPath, hopSharedSecrets)
// Allocate zero'd out byte slices to store the final mix header packet
// and the hmac for each hop.
var (
mixHeader [routingInfoSize]byte
nextHmac [HMACSize]byte
hopPayloadBuf bytes.Buffer
)
// Fill the packet using the caller specified methodology.
if err := pktFiller(sessionKey, &mixHeader); err != nil {
return nil, err
}
// Now we compute the routing information for each hop, along with a
// MAC of the routing info using the shared key for that hop.
for i := numHops - 1; i >= 0; i-- {
// We'll derive the two keys we need for each hop in order to:
// generate our stream cipher bytes for the mixHeader, and
// calculate the MAC over the entire constructed packet.
rhoKey := generateKey("rho", &hopSharedSecrets[i])
muKey := generateKey("mu", &hopSharedSecrets[i])
// The HMAC for the final hop is simply zeroes. This allows the
// last hop to recognize that it is the destination for a
// particular payment.
paymentPath[i].HopPayload.HMAC = nextHmac
// Next, using the key dedicated for our stream cipher, we'll
// generate enough bytes to obfuscate this layer of the onion
// packet.
streamBytes := generateCipherStream(rhoKey, routingInfoSize)
payload := paymentPath[i].HopPayload
// Before we assemble the packet, we'll shift the current
// mix-header to the right in order to make room for this next
// per-hop data.
shiftSize := payload.NumBytes()
rightShift(mixHeader[:], shiftSize)
err := payload.Encode(&hopPayloadBuf)
if err != nil {
return nil, err
}
copy(mixHeader[:], hopPayloadBuf.Bytes())
// Once the packet for this hop has been assembled, we'll
// re-encrypt the packet by XOR'ing with a stream of bytes
// generated using our shared secret.
xor(mixHeader[:], mixHeader[:], streamBytes[:])
// If this is the "last" hop, then we'll override the tail of
// the hop data.
if i == numHops-1 {
copy(mixHeader[len(mixHeader)-len(filler):], filler)
}
// The packet for this hop consists of: mixHeader. When
// calculating the MAC, we'll also include the optional
// associated data which can allow higher level applications to
// prevent replay attacks.
packet := append(mixHeader[:], assocData...)
nextHmac = calcMac(muKey, packet)
hopPayloadBuf.Reset()
}
return &OnionPacket{
Version: baseVersion,
EphemeralKey: sessionKey.PubKey(),
RoutingInfo: mixHeader,
HeaderMAC: nextHmac,
}, nil
}
// rightShift shifts the byte-slice by the given number of bytes to the right
// and 0-fill the resulting gap.
func rightShift(slice []byte, num int) {
for i := len(slice) - num - 1; i >= 0; i-- {
slice[num+i] = slice[i]
}
for i := 0; i < num; i++ {
slice[i] = 0
}
}
// generateHeaderPadding derives the bytes for padding the mix header to ensure
// it remains fixed sized throughout route transit. At each step, we add
// 'frameSize*frames' padding of zeroes, concatenate it to the previous filler,
// then decrypt it (XOR) with the secret key of the current hop. When
// encrypting the mix header we essentially do the reverse of this operation:
// we "encrypt" the padding, and drop 'frameSize*frames' number of zeroes. As
// nodes process the mix header they add the padding ('frameSize*frames') in
// order to check the MAC and decrypt the next routing information eventually
// leaving only the original "filler" bytes produced by this function at the
// last hop. Using this methodology, the size of the field stays constant at
// each hop.
func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash256) []byte {
numHops := path.TrueRouteLength()
// We have to generate a filler that matches all but the last hop (the
// last hop won't generate an HMAC)
fillerSize := path.TotalPayloadSize() - path[numHops-1].HopPayload.NumBytes()
filler := make([]byte, fillerSize)
for i := 0; i < numHops-1; i++ {
// Sum up how many frames were used by prior hops.
fillerStart := routingInfoSize
for _, p := range path[:i] {
fillerStart -= p.HopPayload.NumBytes()
}
// The filler is the part dangling off of the end of the
// routingInfo, so offset it from there, and use the current
// hop's frame count as its size.
fillerEnd := routingInfoSize + path[i].HopPayload.NumBytes()
streamKey := generateKey(key, &sharedSecrets[i])
streamBytes := generateCipherStream(streamKey, numStreamBytes)
xor(filler, filler, streamBytes[fillerStart:fillerEnd])
}
return filler
}
// Encode serializes the raw bytes of the onion packet into the passed
// io.Writer. The form encoded within the passed io.Writer is suitable for
// either storing on disk, or sending over the network.
func (f *OnionPacket) Encode(w io.Writer) error {
ephemeral := f.EphemeralKey.SerializeCompressed()
if _, err := w.Write([]byte{f.Version}); err != nil {
return err
}
if _, err := w.Write(ephemeral); err != nil {
return err
}
if _, err := w.Write(f.RoutingInfo[:]); err != nil {
return err
}
if _, err := w.Write(f.HeaderMAC[:]); err != nil {
return err
}
return nil
}
// Decode fully populates the target ForwardingMessage from the raw bytes
// encoded within the io.Reader. In the case of any decoding errors, an error
// will be returned. If the method success, then the new OnionPacket is ready
// to be processed by an instance of SphinxNode.
func (f *OnionPacket) Decode(r io.Reader) error {
var err error
var buf [1]byte
if _, err := io.ReadFull(r, buf[:]); err != nil {
return err
}
f.Version = buf[0]
// If version of the onion packet protocol unknown for us than in might
// lead to improperly decoded data.
if f.Version != baseVersion {
return ErrInvalidOnionVersion
}
var ephemeral [33]byte
if _, err := io.ReadFull(r, ephemeral[:]); err != nil {
return err
}
f.EphemeralKey, err = btcec.ParsePubKey(ephemeral[:])
if err != nil {
return ErrInvalidOnionKey
}
if _, err := io.ReadFull(r, f.RoutingInfo[:]); err != nil {
return err
}
if _, err := io.ReadFull(r, f.HeaderMAC[:]); err != nil {
return err
}
return nil
}
// ProcessCode is an enum-like type which describes to the high-level package
// user which action should be taken after processing a Sphinx packet.
type ProcessCode int
const (
// ExitNode indicates that the node which processed the Sphinx packet
// is the destination hop in the route.
ExitNode = iota
// MoreHops indicates that there are additional hops left within the
// route. Therefore the caller should forward the packet to the node
// denoted as the "NextHop".
MoreHops
// Failure indicates that a failure occurred during packet processing.
Failure
)
// String returns a human readable string for each of the ProcessCodes.
func (p ProcessCode) String() string {
switch p {
case ExitNode:
return "ExitNode"
case MoreHops:
return "MoreHops"
case Failure:
return "Failure"
default:
return "Unknown"
}
}
// ProcessedPacket encapsulates the resulting state generated after processing
// an OnionPacket. A processed packet communicates to the caller what action
// should be taken after processing.
type ProcessedPacket struct {
// Action represents the action the caller should take after processing
// the packet.
Action ProcessCode
// ForwardingInstructions is the per-hop payload recovered from the
// initial encrypted onion packet. It details how the packet should be
// forwarded and also includes information that allows the processor of
// the packet to authenticate the information passed within the HTLC.
//
// NOTE: This field will only be populated iff the above Action is
// MoreHops.
ForwardingInstructions *HopData
// Payload is the raw payload as extracted from the packet. If the
// ForwardingInstructions field above is nil, then this is a modern TLV
// payload. As a result, the caller should parse the contents to obtain
// the new set of forwarding instructions.
Payload HopPayload
// NextPacket is the onion packet that should be forwarded to the next
// hop as denoted by the ForwardingInstructions field.
//
// NOTE: This field will only be populated iff the above Action is
// MoreHops.
NextPacket *OnionPacket
}
// Router is an onion router within the Sphinx network. The router is capable
// of processing incoming Sphinx onion packets thereby "peeling" a layer off
// the onion encryption which the packet is wrapped with.
type Router struct {
onionKey SingleKeyECDH
log ReplayLog
}
// NewRouter creates a new instance of a Sphinx onion Router given the node's
// currently advertised onion private key, and the target Bitcoin network.
func NewRouter(nodeKey SingleKeyECDH, log ReplayLog) *Router {
return &Router{
onionKey: nodeKey,
log: log,
}
}
// Start starts / opens the ReplayLog's channeldb and its accompanying
// garbage collector goroutine.
func (r *Router) Start() error {
return r.log.Start()
}
// Stop stops / closes the ReplayLog's channeldb and its accompanying
// garbage collector goroutine.
func (r *Router) Stop() {
r.log.Stop()
}
// processOnionCfg is a set of config values that can be used to modify how an
// onion is processed.
type processOnionCfg struct {
blindingPoint *btcec.PublicKey
}
// ProcessOnionOpt defines the signature of a function option that can be used
// to modify how an onion is processed.
type ProcessOnionOpt func(cfg *processOnionCfg)
// WithBlindingPoint is a function option that can be used to set the blinding
// point to be used when processing an onion.
func WithBlindingPoint(point *btcec.PublicKey) ProcessOnionOpt {
return func(cfg *processOnionCfg) {
cfg.blindingPoint = point
}
}
// ProcessOnionPacket processes an incoming onion packet which has been forward
// to the target Sphinx router. If the encoded ephemeral key isn't on the
// target Elliptic Curve, then the packet is rejected. Similarly, if the
// derived shared secret has been seen before the packet is rejected. If the
// blinded point is specified, then it will be used along with the ephemeral key
// in the onion packet to derive the shared secret. Finally, if the MAC doesn't
// check the packet is again rejected.
//
// In the case of a successful packet processing, and ProcessedPacket struct is
// returned which houses the newly parsed packet, along with instructions on
// what to do next.
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte,
incomingCltv uint32, opts ...ProcessOnionOpt) (*ProcessedPacket,
error) {
cfg := &processOnionCfg{}
for _, o := range opts {
o(cfg)
}
// Compute the shared secret for this onion packet.
sharedSecret, err := r.generateSharedSecret(
onionPkt.EphemeralKey, cfg.blindingPoint,
)
if err != nil {
return nil, err
}
// Additionally, compute the hash prefix of the shared secret, which
// will serve as an identifier for detecting replayed packets.
hashPrefix := hashSharedSecret(&sharedSecret)
// Continue to optimistically process this packet, deferring replay
// protection until the end to reduce the penalty of multiple IO
// operations.
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData)
if err != nil {
return nil, err
}
// Atomically compare this hash prefix with the contents of the on-disk
// log, persisting it only if this entry was not detected as a replay.
if err := r.log.Put(hashPrefix, incomingCltv); err != nil {
return nil, err
}
return packet, nil
}
// ReconstructOnionPacket rederives the subsequent onion packet.
//
// NOTE: This method does not do any sort of replay protection, and should only
// be used to reconstruct packets that were successfully processed previously.
func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte,
opts ...ProcessOnionOpt) (*ProcessedPacket, error) {
cfg := &processOnionCfg{}
for _, o := range opts {
o(cfg)
}
// Compute the shared secret for this onion packet.
sharedSecret, err := r.generateSharedSecret(
onionPkt.EphemeralKey, cfg.blindingPoint,
)
if err != nil {
return nil, err
}
return processOnionPacket(onionPkt, &sharedSecret, assocData)
}
// DecryptBlindedHopData uses the router's private key to decrypt data encrypted
// by the creator of the blinded route.
func (r *Router) DecryptBlindedHopData(ephemPub *btcec.PublicKey,
encryptedData []byte) ([]byte, error) {
return decryptBlindedHopData(r.onionKey, ephemPub, encryptedData)
}
// NextEphemeral computes the next ephemeral key given the current ephemeral
// key and the router's private key.
func (r *Router) NextEphemeral(ephemPub *btcec.PublicKey) (*btcec.PublicKey,
error) {
return NextEphemeral(r.onionKey, ephemPub)
}
// OnionPublicKey returns the public key representing the onion key backing this
// router.
func (r *Router) OnionPublicKey() *btcec.PublicKey {
return r.onionKey.PubKey()
}
// unwrapPacket wraps a layer of the passed onion packet using the specified
// shared secret and associated data. The associated data will be used to check
// the HMAC at each hop to ensure the same data is passed along with the onion
// packet. This function returns the next inner onion packet layer, along with
// the hop data extracted from the outer onion packet.
func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
assocData []byte) (*OnionPacket, *HopPayload, error) {
dhKey := onionPkt.EphemeralKey
routeInfo := onionPkt.RoutingInfo
headerMac := onionPkt.HeaderMAC
// Using the derived shared secret, ensure the integrity of the routing
// information by checking the attached MAC without leaking timing
// information.
message := append(routeInfo[:], assocData...)
calculatedMac := calcMac(generateKey("mu", sharedSecret), message)
if !hmac.Equal(headerMac[:], calculatedMac[:]) {
return nil, nil, ErrInvalidOnionHMAC
}
// Attach the padding zeroes in order to properly strip an encryption
// layer off the routing info revealing the routing information for the
// next hop.
streamBytes := generateCipherStream(
generateKey("rho", sharedSecret), numStreamBytes,
)
zeroBytes := bytes.Repeat([]byte{0}, MaxPayloadSize)
headerWithPadding := append(routeInfo[:], zeroBytes...)
var hopInfo [numStreamBytes]byte
xor(hopInfo[:], headerWithPadding, streamBytes)
// Randomize the DH group element for the next hop using the
// deterministic blinding factor.
blindingFactor := computeBlindingFactor(dhKey, sharedSecret[:])
nextDHKey := blindGroupElement(dhKey, blindingFactor)
// With the MAC checked, and the payload decrypted, we can now parse
// out the payload so we can derive the specified forwarding
// instructions.
var hopPayload HopPayload
if err := hopPayload.Decode(bytes.NewReader(hopInfo[:])); err != nil {
return nil, nil, err
}
// With the necessary items extracted, we'll copy of the onion packet
// for the next node, snipping off our per-hop data.
var nextMixHeader [routingInfoSize]byte
copy(nextMixHeader[:], hopInfo[hopPayload.NumBytes():])
innerPkt := &OnionPacket{
Version: onionPkt.Version,
EphemeralKey: nextDHKey,
RoutingInfo: nextMixHeader,
HeaderMAC: hopPayload.HMAC,
}
return innerPkt, &hopPayload, nil
}
// processOnionPacket performs the primary key derivation and handling of onion
// packets. The processed packets returned from this method should only be used
// if the packet was not flagged as a replayed packet.
func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
assocData []byte) (*ProcessedPacket, error) {
// First, we'll unwrap an initial layer of the onion packet. Typically,
// we'll only have a single layer to unwrap, However, if the sender has
// additional data for us within the Extra Onion Blobs (EOBs), then we
// may have to unwrap additional layers. By default, the inner most
// mix header is the one that we'll want to pass onto the next hop so
// they can properly check the HMAC and unwrap a layer for their
// handoff hop.
innerPkt, outerHopPayload, err := unwrapPacket(
onionPkt, sharedSecret, assocData,
)
if err != nil {
return nil, err
}
// By default we'll assume that there are additional hops in the route.
// However if the uncovered 'nextMac' is all zeroes, then this
// indicates that we're the final hop in the route.
var action ProcessCode = MoreHops
if bytes.Compare(zeroHMAC[:], outerHopPayload.HMAC[:]) == 0 {
action = ExitNode
}
hopData, err := outerHopPayload.HopData()
if err != nil {
return nil, err
}
// Finally, we'll return a fully processed packet with the outer most
// hop data (where the primary forwarding instructions lie) and the
// inner most onion packet that we unwrapped.
return &ProcessedPacket{
Action: action,
ForwardingInstructions: hopData,
Payload: *outerHopPayload,
NextPacket: innerPkt,
}, nil
}
// Tx is a transaction consisting of a number of sphinx packets to be atomically
// written to the replay log. This structure helps to coordinate construction of
// the underlying Batch object, and to ensure that the result of the processing
// is idempotent.
type Tx struct {
// batch is the set of packets to be incrementally processed and
// ultimately committed in this transaction
batch *Batch
// router is a reference to the sphinx router that created this
// transaction. Committing this transaction will utilize this router's
// replay log.
router *Router
// packets contains a potentially sparse list of optimistically processed
// packets for this batch. The contents of a particular index should
// only be accessed if the index is *not* included in the replay set, or
// otherwise failed any other stage of the processing.
packets []ProcessedPacket
sync.Mutex
}
// BeginTxn creates a new transaction that can later be committed back to the
// sphinx router's replay log.
//
// NOTE: The nels parameter should represent the maximum number of that could
// be added to the batch, using sequence numbers that match or exceed this
// value could result in an out-of-bounds panic.
func (r *Router) BeginTxn(id []byte, nels int) *Tx {
return &Tx{
batch: NewBatch(id),
router: r,
packets: make([]ProcessedPacket, nels),
}
}
// ProcessOnionPacket processes an incoming onion packet which has been forward
// to the target Sphinx router. If the encoded ephemeral key isn't on the
// target Elliptic Curve, then the packet is rejected. Similarly, if the
// derived shared secret has been seen before the packet is rejected. If the
// blinded point is specified, then it will be used along with the ephemeral key
// in the onion packet to derive the shared secret. Finally, if the MAC doesn't
// check the packet is again rejected.
//
// In the case of a successful packet processing, and ProcessedPacket struct is
// returned which houses the newly parsed packet, along with instructions on
// what to do next.
func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
assocData []byte, incomingCltv uint32, opts ...ProcessOnionOpt) error {
cfg := &processOnionCfg{}
for _, o := range opts {
o(cfg)
}
// Compute the shared secret for this onion packet.
sharedSecret, err := t.router.generateSharedSecret(
onionPkt.EphemeralKey, cfg.blindingPoint,
)
if err != nil {
return err
}
// Additionally, compute the hash prefix of the shared secret, which
// will serve as an identifier for detecting replayed packets.
hashPrefix := hashSharedSecret(&sharedSecret)
// Continue to optimistically process this packet, deferring replay
// protection until the end to reduce the penalty of multiple IO
// operations.
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData)
if err != nil {
return err
}
t.Lock()
defer t.Unlock()
// Add the hash prefix to pending batch of shared secrets that will be
// written later via Commit().
err = t.batch.Put(seqNum, hashPrefix, incomingCltv)
if err != nil {
return err
}
// If we successfully added this packet to the batch, cache the
// processed packet within the Tx which can be accessed after
// committing if this sequence number does not appear in the replay
// set.
t.packets[seqNum] = *packet
return nil
}
// Commit writes this transaction's batch of sphinx packets to the replay log,
// performing a final check against the log for replays.
func (t *Tx) Commit() ([]ProcessedPacket, *ReplaySet, error) {
if t.batch.IsCommitted {
return t.packets, t.batch.ReplaySet, nil
}
rs, err := t.router.log.PutBatch(t.batch)
return t.packets, rs, err
}