Skip to content

Commit 8d8737e

Browse files
committed
add fat error encryption and decryption
1 parent 568d591 commit 8d8737e

7 files changed

+776
-0
lines changed

fat_error_crypto.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package sphinx
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/binary"
7+
)
8+
9+
var byteOrder = binary.BigEndian
10+
11+
type FatErrorStructure struct {
12+
MaxHops int
13+
MaxPayloadSize int
14+
}
15+
16+
type fatErrorBase struct {
17+
maxHops int
18+
totalHmacs int
19+
allHmacsLen int
20+
hmacsAndPayloadsLen int
21+
allPayloadsLen int
22+
payloadLen int
23+
payloadDataLen int
24+
}
25+
26+
func newFatErrorBase(structure *FatErrorStructure) fatErrorBase {
27+
var (
28+
payloadDataLen = structure.MaxPayloadSize
29+
30+
// payloadLen is the size of the per-node payload. It consists of a 1-byte
31+
// payload type followed by the payload data.
32+
payloadLen = 1 + payloadDataLen
33+
34+
totalHmacs = (structure.MaxHops * (structure.MaxHops + 1)) / 2
35+
allHmacsLen = totalHmacs * sha256.Size
36+
allPayloadsLen = payloadLen * structure.MaxHops
37+
hmacsAndPayloadsLen = allHmacsLen + allPayloadsLen
38+
)
39+
40+
return fatErrorBase{
41+
totalHmacs: totalHmacs,
42+
allHmacsLen: allHmacsLen,
43+
hmacsAndPayloadsLen: hmacsAndPayloadsLen,
44+
allPayloadsLen: allPayloadsLen,
45+
maxHops: structure.MaxHops,
46+
payloadLen: payloadLen,
47+
payloadDataLen: payloadDataLen,
48+
}
49+
}
50+
51+
// getMsgComponents splits a complete failure message into its components
52+
// without re-allocating memory.
53+
func (o *fatErrorBase) getMsgComponents(data []byte) ([]byte, []byte, []byte) {
54+
payloads := data[len(data)-o.hmacsAndPayloadsLen : len(data)-o.allHmacsLen]
55+
hmacs := data[len(data)-o.allHmacsLen:]
56+
message := data[:len(data)-o.hmacsAndPayloadsLen]
57+
58+
return message, payloads, hmacs
59+
}
60+
61+
// calculateHmac calculates an hmac given a shared secret and a presumed
62+
// position in the path. Position is expressed as the distance to the error
63+
// source. The error source itself is at position 0.
64+
func (o *fatErrorBase) calculateHmac(sharedSecret Hash256, position int,
65+
message, payloads, hmacs []byte) []byte {
66+
67+
umKey := generateKey("um", &sharedSecret)
68+
hash := hmac.New(sha256.New, umKey[:])
69+
70+
// Include message.
71+
_, _ = hash.Write(message)
72+
73+
// Include payloads including our own.
74+
_, _ = hash.Write(payloads[:(o.maxHops-position)*o.payloadLen])
75+
76+
// Include downstream hmacs.
77+
var hmacsIdx = position + o.maxHops
78+
for j := 0; j < o.maxHops-position-1; j++ {
79+
_, _ = hash.Write(
80+
hmacs[hmacsIdx*sha256.Size : (hmacsIdx+1)*sha256.Size],
81+
)
82+
83+
hmacsIdx += o.maxHops - j - 1
84+
}
85+
86+
return hash.Sum(nil)
87+
}

fat_error_crypto_test.go

+307
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
package sphinx
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"encoding/json"
7+
"os"
8+
"testing"
9+
10+
"github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
var fatErrorTestStructure = &FatErrorStructure{
15+
MaxHops: 27,
16+
MaxPayloadSize: 8,
17+
}
18+
19+
// TestFatOnionFailure checks the ability of sender of payment to decode the
20+
// obfuscated onion error.
21+
func TestFatOnionFailure(t *testing.T) {
22+
t.Parallel()
23+
24+
// Create numHops random sphinx paymentPath.
25+
sessionKey, paymentPath := generateRandomPath(t)
26+
27+
// Reduce the error path on one node, in order to check that we are
28+
// able to receive the error not only from last hop.
29+
errorPath := paymentPath[:len(paymentPath)-1]
30+
31+
failureData := bytes.Repeat([]byte{'A'}, minOnionErrorLength)
32+
sharedSecrets, err := generateSharedSecrets(paymentPath, sessionKey)
33+
require.NoError(t, err)
34+
35+
// Emulate creation of the obfuscator on node where error have occurred.
36+
obfuscator := NewOnionFatErrorEncrypter(
37+
sharedSecrets[len(errorPath)-1], fatErrorTestStructure,
38+
)
39+
40+
// Emulate the situation when last hop creates the onion failure
41+
// message and send it back.
42+
finalPayload := [8]byte{1}
43+
obfuscatedData, err := obfuscator.EncryptError(
44+
true, failureData, finalPayload[:],
45+
)
46+
require.NoError(t, err)
47+
payloads := [][]byte{finalPayload[:]}
48+
49+
// Emulate that failure message is backward obfuscated on every hop.
50+
for i := len(errorPath) - 2; i >= 0; i-- {
51+
// Emulate creation of the obfuscator on forwarding node which
52+
// propagates the onion failure.
53+
obfuscator = NewOnionFatErrorEncrypter(
54+
sharedSecrets[i], fatErrorTestStructure,
55+
)
56+
57+
intermediatePayload := [8]byte{byte(100 + i)}
58+
obfuscatedData, err = obfuscator.EncryptError(
59+
false, obfuscatedData, intermediatePayload[:],
60+
)
61+
require.NoError(t, err)
62+
63+
payloads = append([][]byte{intermediatePayload[:]}, payloads...)
64+
}
65+
66+
// Emulate creation of the deobfuscator on the receiving onion error side.
67+
deobfuscator := NewOnionFatErrorDecrypter(&Circuit{
68+
SessionKey: sessionKey,
69+
PaymentPath: paymentPath,
70+
}, fatErrorTestStructure)
71+
72+
// Emulate that sender node receive the failure message and trying to
73+
// unwrap it, by applying obfuscation and checking the hmac.
74+
decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
75+
require.NoError(t, err)
76+
77+
// We should understand the node from which error have been received.
78+
require.Equal(t,
79+
errorPath[len(errorPath)-1].SerializeCompressed(),
80+
decryptedError.Sender.SerializeCompressed())
81+
82+
require.Equal(t, len(errorPath), decryptedError.SenderIdx)
83+
84+
// Check that message have been properly de-obfuscated.
85+
require.Equal(t, failureData, decryptedError.Message)
86+
require.Equal(t, payloads, decryptedError.Payloads)
87+
}
88+
89+
// TestOnionFailureCorruption checks the ability of sender of payment to
90+
// identify a node on the path that corrupted the failure message.
91+
func TestOnionFailureCorruption(t *testing.T) {
92+
t.Parallel()
93+
94+
// Create numHops random sphinx paymentPath.
95+
sessionKey, paymentPath := generateRandomPath(t)
96+
97+
// Reduce the error path on one node, in order to check that we are
98+
// able to receive the error not only from last hop.
99+
errorPath := paymentPath[:len(paymentPath)-1]
100+
101+
failureData := bytes.Repeat([]byte{'A'}, minOnionErrorLength)
102+
sharedSecrets, err := generateSharedSecrets(paymentPath, sessionKey)
103+
require.NoError(t, err)
104+
105+
// Emulate creation of the obfuscator on node where error have occurred.
106+
obfuscator := NewOnionFatErrorEncrypter(
107+
sharedSecrets[len(errorPath)-1], fatErrorTestStructure,
108+
)
109+
110+
// Emulate the situation when last hop creates the onion failure
111+
// message and send it back.
112+
payload := [8]byte{1}
113+
obfuscatedData, err := obfuscator.EncryptError(true, failureData, payload[:])
114+
require.NoError(t, err)
115+
116+
// Emulate that failure message is backward obfuscated on every hop.
117+
for i := len(errorPath) - 2; i >= 0; i-- {
118+
// Emulate creation of the obfuscator on forwarding node which
119+
// propagates the onion failure.
120+
obfuscator = NewOnionFatErrorEncrypter(
121+
sharedSecrets[i], fatErrorTestStructure,
122+
)
123+
124+
payload := [8]byte{byte(100 + i)}
125+
obfuscatedData, err = obfuscator.EncryptError(
126+
false, obfuscatedData, payload[:],
127+
)
128+
require.NoError(t, err)
129+
130+
// Hop 1 (the second hop from the sender pov) is corrupting the failure
131+
// message.
132+
if i == 1 {
133+
obfuscatedData[0] ^= 255
134+
}
135+
}
136+
137+
// Emulate creation of the deobfuscator on the receiving onion error side.
138+
deobfuscator := NewOnionFatErrorDecrypter(&Circuit{
139+
SessionKey: sessionKey,
140+
PaymentPath: paymentPath,
141+
}, fatErrorTestStructure)
142+
143+
// Emulate that sender node receive the failure message and trying to
144+
// unwrap it, by applying obfuscation and checking the hmac.
145+
decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
146+
require.NoError(t, err)
147+
148+
// Assert that the second hop is correctly identified as the error source.
149+
require.Equal(t, 2, decryptedError.SenderIdx)
150+
require.Nil(t, decryptedError.Message)
151+
}
152+
153+
type specHop struct {
154+
SharedSecret string `json:"sharedSecret"`
155+
EncryptedMessage string `json:"encryptedMessage"`
156+
}
157+
158+
type specVector struct {
159+
EncodedFailureMessage string `json:"encodedFailureMessage"`
160+
161+
Hops []specHop `json:"hops"`
162+
}
163+
164+
// TestOnionFailureSpecVector checks that onion error corresponds to the
165+
// specification.
166+
func TestFatOnionFailureSpecVector(t *testing.T) {
167+
t.Parallel()
168+
169+
vectorBytes, err := os.ReadFile("testdata/fat_error.json")
170+
require.NoError(t, err)
171+
172+
var vector specVector
173+
require.NoError(t, json.Unmarshal(vectorBytes, &vector))
174+
175+
failureData, err := hex.DecodeString(vector.EncodedFailureMessage)
176+
require.NoError(t, err)
177+
178+
paymentPath, err := getSpecPubKeys()
179+
require.NoError(t, err)
180+
181+
sessionKey, err := getSpecSessionKey()
182+
require.NoError(t, err)
183+
184+
var obfuscatedData []byte
185+
sharedSecrets, err := generateSharedSecrets(paymentPath, sessionKey)
186+
require.NoError(t, err)
187+
188+
for i, test := range vector.Hops {
189+
// Decode the shared secret and check that it matchs with
190+
// specification.
191+
expectedSharedSecret, err := hex.DecodeString(test.SharedSecret)
192+
require.NoError(t, err)
193+
194+
obfuscator := NewOnionFatErrorEncrypter(
195+
sharedSecrets[len(sharedSecrets)-1-i], fatErrorTestStructure,
196+
)
197+
198+
require.Equal(t, expectedSharedSecret, obfuscator.sharedSecret[:])
199+
200+
payload := [8]byte{0, 0, 0, 0, 0, 0, 0, byte(i + 1)}
201+
202+
if i == 0 {
203+
// Emulate the situation when last hop creates the onion failure
204+
// message and send it back.
205+
obfuscatedData, err = obfuscator.EncryptError(
206+
true, failureData, payload[:],
207+
)
208+
require.NoError(t, err)
209+
} else {
210+
// Emulate the situation when forward node obfuscates
211+
// the onion failure.
212+
obfuscatedData, err = obfuscator.EncryptError(
213+
false, obfuscatedData, payload[:],
214+
)
215+
require.NoError(t, err)
216+
}
217+
218+
// Decode the obfuscated data and check that it matches the
219+
// specification.
220+
expectedEncryptErrorData, err := hex.DecodeString(test.EncryptedMessage)
221+
require.NoError(t, err)
222+
require.Equal(t, expectedEncryptErrorData, obfuscatedData)
223+
}
224+
225+
deobfuscator := NewOnionFatErrorDecrypter(&Circuit{
226+
SessionKey: sessionKey,
227+
PaymentPath: paymentPath,
228+
}, fatErrorTestStructure)
229+
230+
// Emulate that sender node receives the failure message and trying to
231+
// unwrap it, by applying obfuscation and checking the hmac.
232+
decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
233+
require.NoError(t, err)
234+
235+
// Check that message have been properly de-obfuscated.
236+
require.Equal(t, decryptedError.Message, failureData)
237+
238+
// We should understand the node from which error have been received.
239+
require.Equal(t,
240+
decryptedError.Sender.SerializeCompressed(),
241+
paymentPath[len(paymentPath)-1].SerializeCompressed(),
242+
)
243+
244+
require.Equal(t, len(paymentPath), decryptedError.SenderIdx)
245+
}
246+
247+
// TestFatOnionFailureZeroesMessage checks that a garbage failure is attributed
248+
// to the first hop.
249+
func TestFatOnionFailureZeroesMessage(t *testing.T) {
250+
t.Parallel()
251+
252+
// Create numHops random sphinx paymentPath.
253+
sessionKey, paymentPath := generateRandomPath(t)
254+
255+
// Emulate creation of the deobfuscator on the receiving onion error side.
256+
deobfuscator := NewOnionFatErrorDecrypter(&Circuit{
257+
SessionKey: sessionKey,
258+
PaymentPath: paymentPath,
259+
}, fatErrorTestStructure)
260+
261+
// Emulate that sender node receive the failure message and trying to
262+
// unwrap it, by applying obfuscation and checking the hmac.
263+
obfuscatedData := make([]byte, 20000)
264+
265+
decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
266+
require.NoError(t, err)
267+
268+
require.Equal(t, 1, decryptedError.SenderIdx)
269+
}
270+
271+
// TestFatOnionFailureShortMessage checks that too short failure is attributed
272+
// to the first hop.
273+
func TestFatOnionFailureShortMessage(t *testing.T) {
274+
t.Parallel()
275+
276+
// Create numHops random sphinx paymentPath.
277+
sessionKey, paymentPath := generateRandomPath(t)
278+
279+
// Emulate creation of the deobfuscator on the receiving onion error side.
280+
deobfuscator := NewOnionFatErrorDecrypter(&Circuit{
281+
SessionKey: sessionKey,
282+
PaymentPath: paymentPath,
283+
}, fatErrorTestStructure)
284+
285+
// Emulate that sender node receive the failure message and trying to
286+
// unwrap it, by applying obfuscation and checking the hmac.
287+
obfuscatedData := make([]byte, deobfuscator.hmacsAndPayloadsLen-1)
288+
289+
decryptedError, err := deobfuscator.DecryptError(obfuscatedData)
290+
require.NoError(t, err)
291+
292+
require.Equal(t, 1, decryptedError.SenderIdx)
293+
}
294+
295+
func generateRandomPath(t *testing.T) (*btcec.PrivateKey, []*btcec.PublicKey) {
296+
paymentPath := make([]*btcec.PublicKey, 5)
297+
for i := 0; i < len(paymentPath); i++ {
298+
privKey, err := btcec.NewPrivateKey()
299+
require.NoError(t, err)
300+
301+
paymentPath[i] = privKey.PubKey()
302+
}
303+
304+
sessionKey, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{'A'}, 32))
305+
306+
return sessionKey, paymentPath
307+
}

0 commit comments

Comments
 (0)