Skip to content

Commit

Permalink
convert to fat errors
Browse files Browse the repository at this point in the history
  • Loading branch information
joostjager committed Nov 10, 2022
1 parent b62f49f commit bb61d8d
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 28 deletions.
220 changes: 192 additions & 28 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"errors"
"fmt"

"github.com/aead/chacha20"
Expand Down Expand Up @@ -249,10 +248,11 @@ const onionErrorLength = 2 + 2 + 256 + sha256.Size
func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
*DecryptedError, error) {

// Ensure the error message length is as expected.
if len(encryptedData) != onionErrorLength {
// Ensure the error message length is enough to contain the payloads and
// hmacs blocks.
if len(encryptedData) < hmacsAndPayloadsLen {
return nil, fmt.Errorf("invalid error length: "+
"expected %v got %v", onionErrorLength,
"expected at least %v got %v", hmacsAndPayloadsLen,
len(encryptedData))
}

Expand Down Expand Up @@ -292,30 +292,40 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
// encryption from the encrypted error payload.
encryptedData = onionEncrypt(&sharedSecret, encryptedData)

// Next, we'll need to separate the data, from the MAC itself
// so we can reconstruct and verify it.
expectedMac := encryptedData[:sha256.Size]
data := encryptedData[sha256.Size:]

// With the data split, we'll now re-generate the MAC using its
// specified key.
umKey := generateKey("um", &sharedSecret)
h := hmac.New(sha256.New, umKey[:])
h.Write(data)

// If the MAC matches up, then we've found the sender of the
// error and have also obtained the fully decrypted message.
realMac := h.Sum(nil)
if hmac.Equal(realMac, expectedMac) && sender == 0 {
message, payloads, hmacs := getMsgComponents(encryptedData)

final := payloads[0] == payloadFinal
// TODO: Extract hold time from payload.

expectedHmac := calculateHmac(sharedSecret, i, message, payloads, hmacs)
actualHmac := hmacs[i*sha256.Size : (i+1)*sha256.Size]

// If the hmac does not match up, exit with a nil message.
if !bytes.Equal(actualHmac, expectedHmac[:]) && sender == 0 {
sender = i + 1
msg = data
msg = nil
}

// If we are at the node that is the source of the error, we can now
// save the message in our return variable.
if final && sender == 0 {
sender = i + 1
msg = message
}

// Shift payloads and hmacs to the left to prepare for the next
// iteration.
shiftPayloadsLeft(payloads)
shiftHmacsLeft(hmacs)
}

// If the sender index is still zero, then we haven't found the sender,
// meaning we've failed to decrypt.
// If the sender index is still zero, all hmacs checked out but none of the
// payloads was a final payload. In this case we must be dealing with a max
// length route and a final hop that returned an intermediate payload. Blame
// the final hop.
if sender == 0 {
return nil, errors.New("unable to retrieve onion failure")
sender = NumMaxHops
msg = nil
}

return &DecryptedError{
Expand All @@ -325,6 +335,132 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
}, nil
}

const (
totalHmacs = (NumMaxHops * (NumMaxHops + 1)) / 2
allHmacsLen = totalHmacs * sha256.Size
hmacsAndPayloadsLen = allHmacsLen + allPayloadsLen

// payloadLen is the size of the per-node payload. It consists of a 1-byte
// payload type and an 8-byte hold time.
payloadLen = 1 + 8

allPayloadsLen = payloadLen * NumMaxHops

payloadFinal = 1
payloadIntermediate = 0
)

func shiftHmacsRight(hmacs []byte) {
if len(hmacs) != allHmacsLen {
panic("invalid hmac block length")
}

srcIdx := totalHmacs - 2
destIdx := totalHmacs - 1
copyLen := 1
for i := 0; i < NumMaxHops-1; i++ {
copy(hmacs[destIdx*sha256.Size:], hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size])

copyLen++

srcIdx -= copyLen + 1
destIdx -= copyLen
}
}

func shiftHmacsLeft(hmacs []byte) {
if len(hmacs) != allHmacsLen {
panic("invalid hmac block length")
}

srcIdx := NumMaxHops
destIdx := 1
copyLen := NumMaxHops - 1
for i := 0; i < NumMaxHops-1; i++ {
copy(hmacs[destIdx*sha256.Size:], hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size])

srcIdx += copyLen
destIdx += copyLen + 1
copyLen--
}
}

func shiftPayloadsRight(payloads []byte) {
if len(payloads) != allPayloadsLen {
panic("invalid payload block length")
}

copy(payloads[payloadLen:], payloads)
}

func shiftPayloadsLeft(payloads []byte) {
if len(payloads) != allPayloadsLen {
panic("invalid payload block length")
}

copy(payloads, payloads[payloadLen:NumMaxHops*payloadLen])
}

// getMsgComponents splits a complete failure message into its components
// without re-allocating memory.
func getMsgComponents(data []byte) ([]byte, []byte, []byte) {
payloads := data[len(data)-hmacsAndPayloadsLen : len(data)-allHmacsLen]
hmacs := data[len(data)-allHmacsLen:]
message := data[:len(data)-hmacsAndPayloadsLen]

return message, payloads, hmacs
}

// calculateHmac calculates an hmac given a shared secret and a presumed
// position in the path. Position is expressed as the distance to the error
// source. The error source itself is at position 0.
func calculateHmac(sharedSecret Hash256, position int,
message, payloads, hmacs []byte) []byte {

var dataToHmac []byte

// Include payloads including our own.
dataToHmac = append(dataToHmac, payloads[:(NumMaxHops-position)*payloadLen]...)

// Include downstream hmacs.
var downstreamHmacsIdx = position + NumMaxHops
for j := 0; j < NumMaxHops-position-1; j++ {
dataToHmac = append(dataToHmac, hmacs[downstreamHmacsIdx*sha256.Size:(downstreamHmacsIdx+1)*sha256.Size]...)

downstreamHmacsIdx += NumMaxHops - j - 1
}

// Include message.
dataToHmac = append(dataToHmac, message...)

// Calculate and return hmac.
umKey := generateKey("um", &sharedSecret)
hash := hmac.New(sha256.New, umKey[:])
hash.Write(dataToHmac)

return hash.Sum(nil)
}

// calculateHmac calculates an hmac using the shared secret for this
// OnionErrorEncryptor instance.
func (o *OnionErrorEncrypter) calculateHmac(position int,
message, payloads, hmacs []byte) []byte {

return calculateHmac(o.sharedSecret, position, message, payloads, hmacs)
}

// addHmacs updates the failure data with a series of hmacs corresponding to all
// possible positions in the path for the current node.
func (o *OnionErrorEncrypter) addHmacs(data []byte) {
message, payloads, hmacs := getMsgComponents(data)

for i := 0; i < NumMaxHops; i++ {
hmac := o.calculateHmac(i, message, payloads, hmacs)

copy(hmacs[i*sha256.Size:], hmac)
}
}

// EncryptError is used to make data obfuscation using the generated shared
// secret.
//
Expand All @@ -338,12 +474,40 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
// failure and its origin.
func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []byte {
if initial {
umKey := generateKey("um", &o.sharedSecret)
hash := hmac.New(sha256.New, umKey[:])
hash.Write(data)
h := hash.Sum(nil)
data = append(h, data...)
data = o.initializePayload(data)
} else {
o.addIntermediatePayload(data)
}

// Update hmac block.
o.addHmacs(data)

// Obfuscate.
return onionEncrypt(&o.sharedSecret, data)
}

func (o *OnionErrorEncrypter) initializePayload(message []byte) []byte {
// Add space for payloads and hmacs.
data := make([]byte, len(message)+hmacsAndPayloadsLen)
copy(data, message)

_, payloads, _ := getMsgComponents(data)

// Signal final hops in the payload.
// TODO: Add hold time to payload.
payloads[0] = payloadFinal

return data
}

func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte) {
_, payloads, hmacs := getMsgComponents(data)

// Shift hmacs and payloads to create space for the payload.
shiftPayloadsRight(payloads)
shiftHmacsRight(hmacs)

// Signal intermediate hop in the payload.
// TODO: Add hold time to payload.
payloads[0] = payloadIntermediate
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/stretchr/testify v1.8.1
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
)

Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
Expand Down Expand Up @@ -53,6 +54,15 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
Expand Down Expand Up @@ -85,9 +95,13 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
66 changes: 66 additions & 0 deletions obfuscation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/stretchr/testify/require"
)

// TestOnionFailure checks the ability of sender of payment to decode the
Expand Down Expand Up @@ -85,6 +86,68 @@ func TestOnionFailure(t *testing.T) {
}
}

// TestOnionFailureCorruption checks the ability of sender of payment to
// identify a node on the path that corrupted the failure message.
func TestOnionFailureCorruption(t *testing.T) {
// Create numHops random sphinx paymentPath.
paymentPath := make([]*btcec.PublicKey, 5)
for i := 0; i < len(paymentPath); i++ {
privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)

paymentPath[i] = privKey.PubKey()
}
sessionKey, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{'A'}, 32))

// Reduce the error path on one node, in order to check that we are
// able to receive the error not only from last hop.
errorPath := paymentPath[:len(paymentPath)-1]

failureData := bytes.Repeat([]byte{'A'}, onionErrorLength-sha256.Size)
sharedSecrets, err := generateSharedSecrets(paymentPath, sessionKey)
require.NoError(t, err)

// Emulate creation of the obfuscator on node where error have occurred.
obfuscator := &OnionErrorEncrypter{
sharedSecret: sharedSecrets[len(errorPath)-1],
}

// Emulate the situation when last hop creates the onion failure
// message and send it back.
obfuscatedData := obfuscator.EncryptError(true, failureData)

// Emulate that failure message is backward obfuscated on every hop.
for i := len(errorPath) - 2; i >= 0; i-- {
// Emulate creation of the obfuscator on forwarding node which
// propagates the onion failure.
obfuscator = &OnionErrorEncrypter{
sharedSecret: sharedSecrets[i],
}
obfuscatedData = obfuscator.EncryptError(false, obfuscatedData)

// Hop 1 (the second hop from the sender pov) is corrupting the failure
// message.
if i == 1 {
obfuscatedData[0] ^= 255
}
}

// Emulate creation of the deobfuscator on the receiving onion error side.
deobfuscator := NewOnionErrorDecrypter(&Circuit{
SessionKey: sessionKey,
PaymentPath: paymentPath,
})

// Emulate that sender node receive the failure message and trying to
// unwrap it, by applying obfuscation and checking the hmac.
decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
require.NoError(t, err)

// Assert that the second hop is correctly identified as the error source.
require.Equal(t, 2, decryptedError.SenderIdx)
require.Nil(t, decryptedError.Message)
}

// onionErrorData is a specification onion error obfuscation data which is
// produces by another lightning network node.
var onionErrorData = []struct {
Expand Down Expand Up @@ -179,6 +242,9 @@ func getSpecOnionErrorData() ([]byte, error) {
// TestOnionFailureSpecVector checks that onion error corresponds to the
// specification.
func TestOnionFailureSpecVector(t *testing.T) {
// TODO: Update spec vector.
t.Skip()

failureData, err := getSpecOnionErrorData()
if err != nil {
t.Fatalf("unable to get specification onion failure "+
Expand Down

0 comments on commit bb61d8d

Please sign in to comment.