Skip to content

Commit dd06512

Browse files
committed
sphinx: update onion error encrypter for attributable errors
We enhance the existing onion error encrypter to now include the attribution data. This will be needed by upstream peers in order to help verify the error source and hold times.
1 parent 1444273 commit dd06512

File tree

2 files changed

+279
-32
lines changed

2 files changed

+279
-32
lines changed

crypto.go

Lines changed: 263 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"crypto/hmac"
66
"crypto/sha256"
7+
"encoding/binary"
78
"errors"
89
"fmt"
910

@@ -18,6 +19,14 @@ const (
1819
// the onion. Any value lower than 32 will truncate the HMAC both
1920
// during onion creation as well as during the verification.
2021
HMACSize = 32
22+
23+
// AMMAG is the string representation for the ammag key type. Used in
24+
// cypher stream generation.
25+
AMMAG = "ammag"
26+
27+
// AMMAG_EXT is the string representation for the extended ammag key
28+
// type. user in cypher stream generation.
29+
AMMAG_EXT = "ammagext"
2130
)
2231

2332
// chaChaPolyZeroNonce is a slice of zero bytes used in the chacha20poly1305
@@ -97,6 +106,10 @@ type DecryptedError struct {
97106

98107
// Message is the decrypted error message.
99108
Message []byte
109+
110+
// HoldTimes is an array of hold times reported by each node on the error
111+
// path.
112+
HoldTimes []uint32
100113
}
101114

102115
// zeroHMAC is the special HMAC value that allows the final node to determine
@@ -301,10 +314,10 @@ func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) {
301314
// onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a
302315
// stream cipher, calling onionEncrypt on an already encrypted piece of data
303316
// will decrypt it.
304-
func onionEncrypt(sharedSecret *Hash256, data []byte) []byte {
317+
func onionEncrypt(keyType string, sharedSecret *Hash256, data []byte) []byte {
305318
p := make([]byte, len(data))
306319

307-
ammagKey := generateKey("ammag", sharedSecret)
320+
ammagKey := generateKey(keyType, sharedSecret)
308321
streamBytes := generateCipherStream(ammagKey, uint(len(data)))
309322
xor(p, data, streamBytes)
310323

@@ -324,16 +337,20 @@ const minOnionErrorLength = minPaddedOnionErrorLength + sha256.Size
324337
// onion failure is encrypted in backward manner, starting from the node where
325338
// error have occurred. As a result, in order to decrypt the error we need get
326339
// all shared secret and apply decryption in the reverse order. A structure is
327-
// returned that contains the decrypted error message and information on the
328-
// sender.
329-
func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
330-
*DecryptedError, error) {
331-
332-
// Ensure the error message length is as expected.
333-
if len(encryptedData) < minOnionErrorLength {
334-
return nil, fmt.Errorf("invalid error length: "+
335-
"expected at least %v got %v", minOnionErrorLength,
336-
len(encryptedData))
340+
// returned that contains the decrypted error message and information of the
341+
// error sender. We also report the hold times in ms for each hop on the error
342+
// path.
343+
func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte,
344+
attrData []byte) (*DecryptedError, error) {
345+
346+
// Ensure the error message and attribution data length is as expected.
347+
if len(encryptedData) < minOnionErrorLength ||
348+
len(attrData) < o.hmacsAndPayloadsLen() {
349+
350+
return &DecryptedError{
351+
Sender: o.circuit.PaymentPath[0],
352+
SenderIdx: 1,
353+
}, nil
337354
}
338355

339356
sharedSecrets, err := generateSharedSecrets(
@@ -352,10 +369,16 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
352369
)
353370
copy(dummySecret[:], bytes.Repeat([]byte{1}, 32))
354371

372+
// Copy the failure message data in a new variable.
373+
failData := make([]byte, len(encryptedData))
374+
copy(failData, encryptedData)
375+
376+
hopPayloads := make([]uint32, 0)
377+
355378
// We'll iterate a constant amount of hops to ensure that we don't give
356379
// away an timing information pertaining to the position in the route
357380
// that the error emanated from.
358-
for i := 0; i < NumMaxHops; i++ {
381+
for i := 0; i < o.hopCount; i++ {
359382
var sharedSecret Hash256
360383

361384
// If we've already found the sender, then we'll use our dummy
@@ -369,13 +392,54 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
369392
}
370393

371394
// With the shared secret, we'll now strip off a layer of
372-
// encryption from the encrypted error payload.
373-
encryptedData = onionEncrypt(&sharedSecret, encryptedData)
395+
// encryption from the encrypted failure and attribution
396+
// data.
397+
failData = onionEncrypt(AMMAG, &sharedSecret, failData)
398+
attrData = onionEncrypt(AMMAG_EXT, &sharedSecret, attrData)
399+
400+
payloads := o.payloads(attrData)
401+
hmacs := o.hmacs(attrData)
402+
403+
// Let's calculate the HMAC we expect for the corresponding
404+
// payloads.
405+
position := o.hopCount - i - 1
406+
expectedAttrHmac := o.calculateHmac(
407+
sharedSecret, position, failData, payloads, hmacs,
408+
)
409+
410+
// Let's retrieve the actual HMAC from the correct position in
411+
// the HMACs array.
412+
actualAttrHmac := hmacs[i*o.hmacSize : (i+1)*o.hmacSize]
413+
414+
// If the hmac does not match up, exit with a nil message. This
415+
// is not done for the dummy iterations.
416+
if !bytes.Equal(actualAttrHmac, expectedAttrHmac) &&
417+
sender == 0 && i < len(o.circuit.PaymentPath) {
418+
419+
sender = i + 1
420+
msg = nil
421+
}
374422

375-
// Next, we'll need to separate the data, from the MAC itself
376-
// so we can reconstruct and verify it.
377-
expectedMac := encryptedData[:sha256.Size]
378-
data := encryptedData[sha256.Size:]
423+
// Extract the payload and exit with a nil message if it is
424+
// invalid.
425+
holdTime := o.extractPayload(payloads)
426+
if sender == 0 {
427+
// Store hold time reported by this node.
428+
hopPayloads = append(hopPayloads, holdTime)
429+
430+
// Update the message.
431+
msg = failData[sha256.Size:]
432+
}
433+
434+
// Shift payloads and hmacs to the left to prepare for the next
435+
// iteration.
436+
o.shiftPayloadsLeft(payloads)
437+
o.shiftHmacsLeft(hmacs)
438+
439+
// Next, we'll need to separate the failure data, from the MAC
440+
// itself so we can reconstruct and verify it.
441+
expectedMac := failData[:sha256.Size]
442+
data := failData[sha256.Size:]
379443

380444
// With the data split, we'll now re-generate the MAC using its
381445
// specified key.
@@ -399,12 +463,55 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
399463
}
400464

401465
return &DecryptedError{
402-
SenderIdx: sender,
403466
Sender: o.circuit.PaymentPath[sender-1],
467+
SenderIdx: sender,
404468
Message: msg,
469+
HoldTimes: hopPayloads,
405470
}, nil
406471
}
407472

473+
// extractPayload extracts the payload and payload origin information from the
474+
// given byte slice.
475+
func (o *OnionErrorDecrypter) extractPayload(payloadBytes []byte) uint32 {
476+
// Extract payload.
477+
holdTime := binary.BigEndian.Uint32(payloadBytes[0:o.payloadLen()])
478+
479+
return holdTime
480+
}
481+
482+
func (o *OnionErrorDecrypter) shiftPayloadsLeft(payloads []byte) {
483+
copy(payloads, payloads[o.payloadLen():o.hopCount*o.payloadLen()])
484+
}
485+
486+
func (o *OnionErrorDecrypter) shiftHmacsLeft(hmacs []byte) {
487+
// Work from left to right to avoid overwriting data that is still
488+
// needed later on in the shift operation.
489+
srcIdx := o.hopCount
490+
destIdx := 0
491+
copyLen := o.hopCount - 1
492+
for i := 0; i < o.hopCount-1; i++ {
493+
// Clear first hmac slot. This slot is for the position farthest
494+
// away from the error source. Because we are shifting, this
495+
// cannot be relevant.
496+
copy(hmacs[destIdx*o.hmacSize:], o.zeroHmac)
497+
498+
// The hmacs of the downstream hop become the remaining hmacs
499+
// for the current hop.
500+
copy(
501+
hmacs[(destIdx+1)*o.hmacSize:],
502+
hmacs[srcIdx*o.hmacSize:(srcIdx+copyLen)*o.hmacSize],
503+
)
504+
505+
srcIdx += copyLen
506+
destIdx += copyLen + 1
507+
copyLen--
508+
}
509+
510+
// Clear the very last hmac slot. Because we just shifted, the most
511+
// downstream hop can never be the error source.
512+
copy(hmacs[destIdx*o.hmacSize:], o.zeroHmac)
513+
}
514+
408515
// EncryptError is used to make data obfuscation using the generated shared
409516
// secret.
410517
//
@@ -413,17 +520,146 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
413520
// for backward failure obfuscation of the onion failure blob. By obfuscating
414521
// the onion failure on every node in the path we are adding additional step of
415522
// the security and barrier for malware nodes to retrieve valuable information.
416-
// The reason for using onion obfuscation is to not give
417-
// away to the nodes in the payment path the information about the exact
418-
// failure and its origin.
419-
func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []byte {
523+
// The reason for using onion obfuscation is to not give away to the nodes in
524+
// the payment path the information about the exact failure and its origin.
525+
// Every node down the error path reports the recorded hold times for the HTLC,
526+
// so this is also passed as an argument to this function in order for this node
527+
// to append its own value. The attribution data is a structure which helps with
528+
// identifying malicious intermediate hops that may have modified the failure
529+
// data.
530+
func (o *OnionErrorEncrypter) EncryptError(initial bool, legacyData []byte,
531+
attrData []byte, holdTime uint32) ([]byte, []byte, error) {
532+
533+
if initial && attrData != nil {
534+
return nil, nil, fmt.Errorf("unable to encrypt, cannot " +
535+
"initialize error with existing attribution data")
536+
}
537+
538+
if attrData == nil {
539+
attrData = o.initializePayload(holdTime)
540+
}
541+
420542
if initial {
543+
if len(legacyData) < minPaddedOnionErrorLength {
544+
return nil, nil, fmt.Errorf("initial data size less "+
545+
"than %v", minPaddedOnionErrorLength)
546+
}
547+
421548
umKey := generateKey("um", &o.sharedSecret)
422549
hash := hmac.New(sha256.New, umKey[:])
423-
hash.Write(data)
550+
hash.Write(legacyData)
424551
h := hash.Sum(nil)
425-
data = append(h, data...)
552+
legacyData = append(h, legacyData...)
553+
} else {
554+
if len(attrData) < o.hmacsAndPayloadsLen() {
555+
return nil, nil, fmt.Errorf("invalid attribution data"+
556+
"length, have %v expected %v", len(attrData),
557+
o.hmacsAndPayloadsLen())
558+
}
559+
560+
// Add our hold time.
561+
o.addIntermediatePayload(attrData, holdTime)
562+
563+
// Shift hmacs to create space for the new hmacs.
564+
o.shiftHmacsRight(o.hmacs(attrData))
565+
}
566+
567+
// Update hmac block.
568+
o.addHmacs(attrData, legacyData)
569+
570+
legacy := onionEncrypt(AMMAG, &o.sharedSecret, legacyData)
571+
attrError := onionEncrypt(AMMAG_EXT, &o.sharedSecret, attrData)
572+
573+
return legacy, attrError, nil
574+
}
575+
576+
func (o *OnionErrorEncrypter) shiftHmacsRight(hmacs []byte) {
577+
totalHmacs := (o.hopCount * (o.hopCount + 1)) / 2
578+
579+
// Work from right to left to avoid overwriting data that is still
580+
// needed.
581+
srcIdx := totalHmacs - 2
582+
destIdx := totalHmacs - 1
583+
584+
// The variable copyLen contains the number of hmacs to copy for the
585+
// current hop.
586+
copyLen := 1
587+
for i := 0; i < o.hopCount-1; i++ {
588+
// Shift the hmacs to the right for the current hop. The hmac
589+
// corresponding to the assumed position that is farthest away
590+
// from the error source is discarded.
591+
copy(
592+
hmacs[destIdx*o.hmacSize:],
593+
hmacs[srcIdx*o.hmacSize:(srcIdx+copyLen)*o.hmacSize],
594+
)
595+
596+
// The number of hmacs to copy increases by one for each
597+
// iteration. The further away from the error source, the more
598+
// downstream hmacs exist that are relevant.
599+
copyLen++
600+
601+
// Update indices backwards for the next iteration.
602+
srcIdx -= copyLen + 1
603+
destIdx -= copyLen
604+
}
605+
606+
// Zero out the hmac slots corresponding to every possible position
607+
// relative to the error source for the current hop. This is not
608+
// strictly necessary as these slots are overwritten anyway, but we
609+
// clear them for cleanliness.
610+
for i := 0; i < o.hopCount; i++ {
611+
copy(hmacs[i*o.hmacSize:], o.zeroHmac)
612+
}
613+
}
614+
615+
// addHmacs updates the failure data with a series of hmacs corresponding to all
616+
// possible positions in the path for the current node.
617+
func (o *OnionErrorEncrypter) addHmacs(data []byte, message []byte) {
618+
payloads := o.payloads(data)
619+
hmacs := o.hmacs(data)
620+
621+
for i := 0; i < o.hopCount; i++ {
622+
position := o.hopCount - i - 1
623+
hmac := o.calculateHmac(
624+
o.sharedSecret, position, message, payloads, hmacs,
625+
)
626+
627+
copy(hmacs[i*o.hmacSize:], hmac)
426628
}
629+
}
630+
631+
func (o *OnionErrorEncrypter) initializePayload(holdTime uint32) []byte {
632+
633+
// Add space for payloads and hmacs.
634+
data := make([]byte, o.hmacsAndPayloadsLen())
635+
636+
payloads := o.payloads(data)
637+
638+
// Signal final hops in the payload.
639+
addPayload(payloads, holdTime)
640+
641+
return data
642+
}
643+
644+
func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte,
645+
holdTime uint32) {
646+
647+
payloads := o.payloads(data)
648+
649+
// Shift payloads to create space for the new payload.
650+
o.shiftPayloadsRight(payloads)
651+
652+
// Signal intermediate hop in the payload.
653+
addPayload(payloads, holdTime)
654+
}
655+
656+
func (o *OnionErrorEncrypter) shiftPayloadsRight(payloads []byte) {
657+
copy(payloads[o.payloadLen():], payloads)
658+
}
659+
660+
func addPayload(payloads []byte, holdTime uint32) {
427661

428-
return onionEncrypt(&o.sharedSecret, data)
662+
payload := make([]byte, 4)
663+
binary.BigEndian.PutUint32(payload, holdTime)
664+
copy(payloads, payload)
429665
}

0 commit comments

Comments
 (0)