From 4eca0e81e94100050a484a66d12dea0c9f91ff53 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 23 May 2023 14:52:52 +0200 Subject: [PATCH 01/10] sphinx: add minPaddedOnionErrorLength constant --- crypto.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crypto.go b/crypto.go index 6b1b49e..f034764 100644 --- a/crypto.go +++ b/crypto.go @@ -311,10 +311,14 @@ func onionEncrypt(sharedSecret *Hash256, data []byte) []byte { return p } -// minOnionErrorLength is the minimally expected length of the onion error -// message. Including padding, all messages on the wire should be at least 256 -// bytes. We then add the size of the sha256 HMAC as well. -const minOnionErrorLength = 2 + 2 + 256 + sha256.Size +// minPaddedOnionErrorLength is the minimally expected length of the padded +// onion error message including two uint16s for the length of the message and +// the length of the padding. +const minPaddedOnionErrorLength = 2 + 2 + 256 + +// minOnionErrorLength is the minimally expected length of the complete onion +// error message including the HMAC. +const minOnionErrorLength = minPaddedOnionErrorLength + sha256.Size // DecryptError attempts to decrypt the passed encrypted error response. The // onion failure is encrypted in backward manner, starting from the node where From 1dc731c7e69d672559b5c80b9bce953522662991 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 17 Jan 2023 13:44:32 +0100 Subject: [PATCH 02/10] sphinx: initialize NewOnionErrorEncrypter with shared secret directly Allow for more flexible usage of the error encrypter. This is useful when upgrading an existing legacy error encrypter to attributable errors in lnd. --- obfuscation.go | 20 ++------------------ obfuscation_test.go | 17 +++++++---------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/obfuscation.go b/obfuscation.go index a731a6c..207129d 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -12,26 +12,10 @@ type OnionErrorEncrypter struct { sharedSecret Hash256 } -// NewOnionErrorEncrypter creates new instance of the onion encrypter backed by -// the passed router, with encryption to be done using the passed ephemeralKey. -func NewOnionErrorEncrypter(router *Router, ephemeralKey *btcec.PublicKey, - opts ...ProcessOnionOpt) (*OnionErrorEncrypter, error) { - - cfg := &processOnionCfg{} - for _, o := range opts { - o(cfg) - } - - sharedSecret, err := router.generateSharedSecret( - ephemeralKey, cfg.blindingPoint, - ) - if err != nil { - return nil, err - } - +func NewOnionErrorEncrypter(sharedSecret Hash256) *OnionErrorEncrypter { return &OnionErrorEncrypter{ sharedSecret: sharedSecret, - }, nil + } } // Encode writes the encrypter's shared secret to the provided io.Writer. diff --git a/obfuscation_test.go b/obfuscation_test.go index 8c0af83..0051361 100644 --- a/obfuscation_test.go +++ b/obfuscation_test.go @@ -35,9 +35,7 @@ func TestOnionFailure(t *testing.T) { } // Emulate creation of the obfuscator on node where error have occurred. - obfuscator := &OnionErrorEncrypter{ - sharedSecret: sharedSecrets[len(errorPath)-1], - } + obfuscator := NewOnionErrorEncrypter(sharedSecrets[len(errorPath)-1]) // Emulate the situation when last hop creates the onion failure // message and send it back. @@ -47,9 +45,7 @@ func TestOnionFailure(t *testing.T) { 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], - } + obfuscator = NewOnionErrorEncrypter(sharedSecrets[i]) obfuscatedData = obfuscator.EncryptError(false, obfuscatedData) } @@ -208,16 +204,17 @@ func TestOnionFailureSpecVector(t *testing.T) { t.Fatalf("unable to decode spec shared secret: %v", err) } - obfuscator := &OnionErrorEncrypter{ - sharedSecret: sharedSecrets[len(sharedSecrets)-1-i], - } + + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(sharedSecrets)-1-i], + ) var b bytes.Buffer if err := obfuscator.Encode(&b); err != nil { t.Fatalf("unable to encode obfuscator: %v", err) } - obfuscator2 := &OnionErrorEncrypter{} + obfuscator2 := NewOnionErrorEncrypter(Hash256{}) obfuscatorReader := bytes.NewReader(b.Bytes()) if err := obfuscator2.Decode(obfuscatorReader); err != nil { t.Fatalf("unable to decode obfuscator: %v", err) From f7f161968faef4bfe5a3b22694a575e42a2f5094 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 17 Jan 2023 13:51:08 +0100 Subject: [PATCH 03/10] sphinx: export GenerateSharedSecret --- crypto.go | 8 ++++---- sphinx.go | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crypto.go b/crypto.go index f034764..4b7f199 100644 --- a/crypto.go +++ b/crypto.go @@ -234,18 +234,18 @@ func chacha20polyDecrypt(key, cipherTxt []byte) ([]byte, error) { // // TODO(roasbef): rename? type sharedSecretGenerator interface { - // generateSharedSecret given a public key, generates a shared secret + // GenerateSharedSecret given a public key, generates a shared secret // using private data of the underlying sharedSecretGenerator. - generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) + GenerateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) } -// generateSharedSecret generates the shared secret using the given ephemeral +// GenerateSharedSecret generates the shared secret using the given ephemeral // pub key and the Router's private key. If a blindingPoint is provided then it // is used to tweak the Router's private key before creating the shared secret // with the ephemeral pub key. The blinding point is used to determine our // shared secret with the receiver. From that we can determine our shared // secret with the sender using the dhKey. -func (r *Router) generateSharedSecret(dhKey, +func (r *Router) GenerateSharedSecret(dhKey, blindingPoint *btcec.PublicKey) (Hash256, error) { // If no blinding point is provided, then the un-tweaked dhKey can diff --git a/sphinx.go b/sphinx.go index 475f189..2281e0e 100644 --- a/sphinx.go +++ b/sphinx.go @@ -625,7 +625,7 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, } // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret( + sharedSecret, err := r.GenerateSharedSecret( onionPkt.EphemeralKey, cfg.blindingPoint, ) if err != nil { @@ -669,7 +669,7 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte, } // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret( + sharedSecret, err := r.GenerateSharedSecret( onionPkt.EphemeralKey, cfg.blindingPoint, ) if err != nil { @@ -878,7 +878,7 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, } // Compute the shared secret for this onion packet. - sharedSecret, err := t.router.generateSharedSecret( + sharedSecret, err := t.router.GenerateSharedSecret( onionPkt.EphemeralKey, cfg.blindingPoint, ) if err != nil { From 375aada0b9cf540c0eac4ffae15061e397283096 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Mon, 19 May 2025 16:19:31 +0200 Subject: [PATCH 04/10] testdata: update vectors for attributable errors We add the encoded failure message and the intermediate encrypted error messages that correspond to attribution data to the test vector. --- testdata/onion-test.json | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/testdata/onion-test.json b/testdata/onion-test.json index 193256f..5721d3e 100644 --- a/testdata/onion-test.json +++ b/testdata/onion-test.json @@ -1,28 +1,39 @@ { "comment": "A testcase for a variable length hop_payload. The third payload is 275 bytes long.", "generate": { + "encodedFailureMessage": "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "session_key": "4141414141414141414141414141414141414141414141414141414141414141", "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", "hops": [ { "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", - "payload": "1202023a98040205dc06080000000000000001" + "payload": "1202023a98040205dc06080000000000000001", + "sharedSecret": "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328", + "encryptedMessage": "d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb8cf5958e3f3f2a4365d1ec0df1d83c6a6221b5b7d1ff30156a2289a1d3ee559e7c7256bda444bb8e046f860e00b3a59a85e1e1a43de215fd5e6bf646a5deab97b1912c934e31b1cfd344764d6ca7e14ea7b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252" }, { "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", - "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f" + "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f", + "sharedSecret": "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d", + "encryptedMessage": "1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf36dee297079eb841391ccd3096da76461e314863b6412efe0ffe228d51c6097db10d3edb2e50ea679820613bfe9db11ba02920ab4c1f2a79890d997f1fc022f3ab78f0029cc6de0c90be74d55f4a99bf77a50e20f8d076fe61776190a61d2f41c408871c0279309cba3b60fcdc7efc4a0e90b47cb4a418fc78f362ecc7f15ebbce9f854c09c7be300ebc1a40a69d4c7cb7a19779b6905e82bec221a709c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb" }, { "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", - "payload": "12020230d4040204e206080000000000000003" + "payload": "12020230d4040204e206080000000000000003", + "sharedSecret": "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc", + "encryptedMessage": "34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dc641f68e356323d766fff0aaca5039fe7fc27038195844951a97d5a5b26698a4ca1e9cd4bca1fcca0aac5fee91b18977d2ad0e399ba159733fc98f6e96898ebc39bf0028c9c81619233bab6fad0328aa183a635fac20437fa6e00e899b2527c3697a8ab7342e42d55a679b176ab76671fcd480a9894cb897fa6af0a45b917a162bed6c491972403185df7235502f7ada65769d1bfb12d29f10e25b0d3cc08bbf6de8481ac5c04df32b4533b4f764c2aefb7333202645a629fb16e4a208e9045dc36830759c852b31dd613d8b2b10bbead1ed4eb60c85e8a4517deba5ab53e39867c83c26802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a" }, { "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", - "payload": "1202022710040203e806080000000000000004" + "payload": "1202022710040203e806080000000000000004", + "sharedSecret": "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae", + "encryptedMessage": "74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54fef30646ae53ccf0855b61f291a81e0ec6dc0f6bf69f0ca0e5889b7e23f577ba67d2a7d6a2aa91264ab9b20630ed52f8ed56cc10a869807cd1a4c2cd802d8433fee5685d6a04edb0bff248a480b93b01904bed3bb31705d1ecb7332004290cc0cd9cc2f7907cf9db28eec02985301668f53fbc28c3e095c8f3a6cd8cab28e5e442fd9ba608b8b12e098731bbfda755393bd403c62289093b40390b2bae337fc87d2606ca028311d73a9ffbdffef56020c735ada30f54e577c6a9ec515ae2739290609503404b118d7494499ecf0457d75015bb60a16288a4959d74cf5ac5d8d6c113de39f748a418d2a7083b90c9c0a09a49149fd1f2d2cde4412e5aa2421eca6fd4f6fe6b2c362ff37d1a0608c931c7ca3b8fefcfd4c44ef9c38357a0767b14f83cb49bd1989fb3f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1" }, { "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", - "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + "sharedSecret": "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66", + "encryptedMessage": "84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad" } ] }, From 36b88cb9a8f9284eeb0015918d0908c388c8cb24 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Mon, 19 May 2025 20:01:21 +0200 Subject: [PATCH 05/10] sphinx: add attributable error structure We add the AttrErrorStructure which is going to be used in follow-up commits by the updated onion error encrypter and decrypter. --- attr_error_structure.go | 159 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 attr_error_structure.go diff --git a/attr_error_structure.go b/attr_error_structure.go new file mode 100644 index 0000000..6dd4d89 --- /dev/null +++ b/attr_error_structure.go @@ -0,0 +1,159 @@ +// Package sphinx implements the Sphinx onion routing packet format along with +// the attributable error scheme used to propagate and verify failure messages +// on the return path of a route. +package sphinx + +import ( + "crypto/hmac" + "crypto/sha256" + "io" +) + +// AttrErrorStructure contains the parameters that define the structure +// of the error message that is passed back. +type AttrErrorStructure struct { + // hopCount is the assumed maximum number of hops in the path. + hopCount int + + // fixedPayloadLen is the length of the payload data that each hop along + // the route can add. + fixedPayloadLen int + + // hmacSize is the number of bytes that is reserved for each hmac. + hmacSize int + + zeroHmac []byte +} + +// NewAttrErrorStructure creates an AttrErrorStructure with the defined +// parameters and returns it. +func NewAttrErrorStructure(hopCount int, fixedPayloadLen int, + hmacSize int) *AttrErrorStructure { + + return &AttrErrorStructure{ + hopCount: hopCount, + fixedPayloadLen: fixedPayloadLen, + hmacSize: hmacSize, + + zeroHmac: make([]byte, hmacSize), + } +} + +// HopCount returns the assumed maximum number of hops in the path. +func (o *AttrErrorStructure) HopCount() int { + return o.hopCount +} + +// FixedPayloadLen returns the length of the payload data that each hop along +// the route can add. +func (o *AttrErrorStructure) FixedPayloadLen() int { + return o.fixedPayloadLen +} + +// HmacSize returns the number of bytes that is reserved for each hmac. +func (o *AttrErrorStructure) HmacSize() int { + return o.hmacSize +} + +// totalHmacs is the total number of hmacs that is present in the failure +// message. Every hop adds HopCount hmacs to the message, but as the error +// back-propagates, downstream hmacs can be pruned. This results in the number +// of hmacs for each hop decreasing by one for each step that we move away from +// the current node. +func (o *AttrErrorStructure) totalHmacs() int { + // This is the triangular number of hopCount (sum of 1..hopCount). + // + //nolint:mnd + return (o.hopCount * (o.hopCount + 1)) / 2 +} + +// allHmacsLen is the total length in the bytes of all hmacs in the failure +// message. +func (o *AttrErrorStructure) allHmacsLen() int { + return o.totalHmacs() * o.hmacSize +} + +// hmacsAndPayloadsLen is the total length in bytes of all hmacs and payloads +// together. +func (o *AttrErrorStructure) hmacsAndPayloadsLen() int { + return o.allHmacsLen() + o.allPayloadsLen() +} + +// allPayloadsLen is the total length in bytes of all payloads in the failure +// message. +func (o *AttrErrorStructure) allPayloadsLen() int { + return o.payloadLen() * o.hopCount +} + +// payloadLen is the size of the per-node payload. It is fixed and was set when +// instantiating this attr error structure. +func (o *AttrErrorStructure) payloadLen() int { + return o.fixedPayloadLen +} + +// payloads returns a slice containing all payloads in the given failure +// data block. The payloads follow the message in the block. +func (o *AttrErrorStructure) payloads(data []byte) []byte { + dataLen := len(data) + + return data[dataLen-o.hmacsAndPayloadsLen() : dataLen-o.allHmacsLen()] +} + +// hmacs returns a slice containing all hmacs in the given failure data block. +// The hmacs are positioned at the end of the data block. +func (o *AttrErrorStructure) hmacs(data []byte) []byte { + return data[len(data)-o.allHmacsLen():] +} + +// 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 (o *AttrErrorStructure) calculateHmac(sharedSecret Hash256, + position int, message, payloads, hmacs []byte) []byte { + + umKey := generateKey("um", &sharedSecret) + hash := hmac.New(sha256.New, umKey[:]) + + // Include message. + _, _ = hash.Write(message) + + // Include payloads including our own. + _, _ = hash.Write(payloads[:(position+1)*o.payloadLen()]) + + // Include downstream hmacs. + writeDownstreamHmacs(position, o.hopCount, hmacs, o.hmacSize, hash) + + hmac := hash.Sum(nil) + + return hmac[:o.hmacSize] +} + +// writeDownstreamHmacs writes the hmacs of downstream nodes that are relevant +// for the given position to a writer instance. Position is expressed as the +// distance to the error source. The error source itself is at position 0. +func writeDownstreamHmacs(position, maxHops int, hmacs []byte, hmacBytes int, + w io.Writer) { + + // Track the index of the next hmac to write in a variable. The first + // maxHops slots are reserved for the hmacs of the current hop and can + // therefore be skipped. The first hmac to write is part of the block of + // hmacs that was written by the first downstream node. Which hmac + // exactly is determined by the assumed position of the current node. + var hmacIdx = maxHops + (maxHops - position - 1) + + // Iterate over all downstream nodes. + for j := range position { + _, _ = w.Write( + hmacs[hmacIdx*hmacBytes : (hmacIdx+1)*hmacBytes], + ) + + // Calculate the total number of hmacs in the block of the + // current downstream node. + blockSize := maxHops - j - 1 + + // Skip to the next block. The new hmac index will point to the + // hmac that corresponds to the next downstream node which is + // one step closer to the assumed error source. + hmacIdx += blockSize + } +} From 266d192438a23cca51b734db1c7851e0716605d1 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Tue, 20 May 2025 13:15:31 +0200 Subject: [PATCH 06/10] sphinx: update encrypter for attr 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. --- crypto.go | 175 ++++++++++++++++++++++++++++++++++++++++++++++--- error.go | 4 ++ obfuscation.go | 11 +++- 3 files changed, 178 insertions(+), 12 deletions(-) diff --git a/crypto.go b/crypto.go index 4b7f199..54b05a3 100644 --- a/crypto.go +++ b/crypto.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/hmac" "crypto/sha256" + "encoding/binary" "errors" "fmt" @@ -18,6 +19,14 @@ const ( // the onion. Any value lower than 32 will truncate the HMAC both // during onion creation as well as during the verification. HMACSize = 32 + + // Ammag is the string representation for the ammag key type. Used in + // cypher stream generation. + Ammag = "ammag" + + // AmmagExt is the string representation for the extended ammag key + // type. Used in cypher stream generation. + AmmagExt = "ammagext" ) // chaChaPolyZeroNonce is a slice of zero bytes used in the chacha20poly1305 @@ -301,10 +310,10 @@ func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) { // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a // stream cipher, calling onionEncrypt on an already encrypted piece of data // will decrypt it. -func onionEncrypt(sharedSecret *Hash256, data []byte) []byte { +func onionEncrypt(keyType string, sharedSecret *Hash256, data []byte) []byte { p := make([]byte, len(data)) - ammagKey := generateKey("ammag", sharedSecret) + ammagKey := generateKey(keyType, sharedSecret) streamBytes := generateCipherStream(ammagKey, uint(len(data))) xor(p, data, streamBytes) @@ -370,7 +379,9 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( // With the shared secret, we'll now strip off a layer of // encryption from the encrypted error payload. - encryptedData = onionEncrypt(&sharedSecret, encryptedData) + encryptedData = onionEncrypt( + Ammag, &sharedSecret, encryptedData, + ) // Next, we'll need to separate the data, from the MAC itself // so we can reconstruct and verify it. @@ -405,6 +416,18 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( }, nil } +var ( + // errInitWithAttrData is returned when initial error encryption is + // requested while attribution data is already present. + errInitWithAttrData = errors.New("unable to encrypt, cannot " + + "initialize error with existing attribution data") + + // errInitialDataTooSmall is returned when the initial error data is + // smaller than the minimum padded onion error length. + errInitialDataTooSmall = errors.New("initial data size less than " + + "minimum padded onion error length") +) + // EncryptError is used to make data obfuscation using the generated shared // secret. // @@ -413,17 +436,149 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( // for backward failure obfuscation of the onion failure blob. By obfuscating // the onion failure on every node in the path we are adding additional step of // the security and barrier for malware nodes to retrieve valuable information. -// The reason for using onion obfuscation is to not give -// away to the nodes in the payment path the information about the exact -// failure and its origin. -func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []byte { +// The reason for using onion obfuscation is to not give away to the nodes in +// the payment path the information about the exact failure and its origin. +// Every node down the error path reports the recorded hold times for the HTLC, +// so this is also passed as an argument to this function in order for this node +// to append its own value. The attribution data is a structure which helps with +// identifying malicious intermediate hops that may have modified the failure +// data. +func (o *OnionErrorEncrypter) EncryptError(initial bool, legacyData []byte, + attrData []byte, holdTime uint32) ([]byte, []byte, error) { + + if initial && attrData != nil { + return nil, nil, errInitWithAttrData + } + + if attrData == nil { + attrData = o.initializePayload(holdTime) + } + if initial { + if len(legacyData) < minPaddedOnionErrorLength { + return nil, nil, fmt.Errorf("%w: min %d", + errInitialDataTooSmall, + minPaddedOnionErrorLength) + } + umKey := generateKey("um", &o.sharedSecret) hash := hmac.New(sha256.New, umKey[:]) - hash.Write(data) + hash.Write(legacyData) h := hash.Sum(nil) - data = append(h, data...) + legacyData = append(h, legacyData...) + } else { + if len(attrData) < o.hmacsAndPayloadsLen() { + return nil, nil, ErrInvalidAttrStructure + } + + // Add our hold time. + o.addIntermediatePayload(attrData, holdTime) + + // Shift hmacs to create space for the new hmacs. + o.shiftHmacsRight(o.hmacs(attrData)) + } + + // Update hmac block. + o.addHmacs(attrData, legacyData) + + legacy := onionEncrypt(Ammag, &o.sharedSecret, legacyData) + attrError := onionEncrypt(AmmagExt, &o.sharedSecret, attrData) + + return legacy, attrError, nil +} + +// shiftHmacsRight shifts all hmacs one position to the right, making room for +// new hmacs at the front for the current hop. +func (o *OnionErrorEncrypter) shiftHmacsRight(hmacs []byte) { + total := o.totalHmacs() + + // Work from right to left to avoid overwriting data that is still + // needed. + srcIdx := total - 2 //nolint:mnd // Second-to-last hmac slot. + destIdx := total - 1 + + // The variable copyLen contains the number of hmacs to copy for the + // current hop. + copyLen := 1 + for range o.hopCount - 1 { + // Shift the hmacs to the right for the current hop. The hmac + // corresponding to the assumed position that is farthest away + // from the error source is discarded. + copy( + hmacs[destIdx*o.hmacSize:], + hmacs[srcIdx*o.hmacSize:(srcIdx+copyLen)*o.hmacSize], + ) + + // The number of hmacs to copy increases by one for each + // iteration. The further away from the error source, the more + // downstream hmacs exist that are relevant. + copyLen++ + + // Update indices backwards for the next iteration. + srcIdx -= copyLen + 1 + destIdx -= copyLen } - return onionEncrypt(&o.sharedSecret, data) + // Zero out the hmac slots corresponding to every possible position + // relative to the error source for the current hop. This is not + // strictly necessary as these slots are overwritten anyway, but we + // clear them for cleanliness. + for i := range o.hopCount { + copy(hmacs[i*o.hmacSize:], o.zeroHmac) + } +} + +// 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 []byte) { + payloads := o.payloads(data) + hmacs := o.hmacs(data) + + for i := range o.hopCount { + position := o.hopCount - i - 1 + hmac := o.calculateHmac( + o.sharedSecret, position, message, payloads, hmacs, + ) + + copy(hmacs[i*o.hmacSize:], hmac) + } +} + +// initializePayload creates a new attribution data block with the given hold +// time set as the initial payload. +func (o *OnionErrorEncrypter) initializePayload(holdTime uint32) []byte { + // Add space for payloads and hmacs. + data := make([]byte, o.hmacsAndPayloadsLen()) + + payloads := o.payloads(data) + + // Signal final hops in the payload. + addPayload(payloads, holdTime) + + return data +} + +// addIntermediatePayload shifts existing payloads and prepends the given hold +// time as the new first payload entry. +func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte, + holdTime uint32) { + + payloads := o.payloads(data) + + // Shift payloads to create space for the new payload. + o.shiftPayloadsRight(payloads) + + // Signal intermediate hop in the payload. + addPayload(payloads, holdTime) +} + +// shiftPayloadsRight shifts all payloads one position to the right, making +// room for a new payload at the front. +func (o *OnionErrorEncrypter) shiftPayloadsRight(payloads []byte) { + copy(payloads[o.payloadLen():], payloads) +} + +// addPayload writes a hold time value into the first payload slot. +func addPayload(payloads []byte, holdTime uint32) { + binary.BigEndian.PutUint32(payloads, holdTime) } diff --git a/error.go b/error.go index 14cb777..96731a4 100644 --- a/error.go +++ b/error.go @@ -53,4 +53,8 @@ var ( // ErrIOReadFull is returned when an io read full operation fails. ErrIOReadFull = errors.New("io read full error") + + // ErrInvalidAttrStructure is an error that signals that the provided + // attribution data have an invalid length. + ErrInvalidAttrStructure = errors.New("invalid attribution data length") ) diff --git a/obfuscation.go b/obfuscation.go index 207129d..8f14989 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -9,12 +9,19 @@ import ( // OnionErrorEncrypter is a struct that's used to implement onion error // encryption as defined within BOLT0004. type OnionErrorEncrypter struct { + *AttrErrorStructure + sharedSecret Hash256 } -func NewOnionErrorEncrypter(sharedSecret Hash256) *OnionErrorEncrypter { +// NewOnionErrorEncrypter creates a new encrypter with the provided shared +// secret and attributable error structure. +func NewOnionErrorEncrypter(sharedSecret Hash256, + structure *AttrErrorStructure) *OnionErrorEncrypter { + return &OnionErrorEncrypter{ - sharedSecret: sharedSecret, + sharedSecret: sharedSecret, + AttrErrorStructure: structure, } } From 007ed748f105ee7a8d3722c16b0ed09ac6b807d9 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Tue, 20 May 2025 13:19:17 +0200 Subject: [PATCH 07/10] sphinx: update decrypter for attr errors We enhance the decrypter to now also use attr data. This structure is now being decrypted alongside the legacy error message. If any of the two fields fails to decrypt we stop the decryption process and blame the further decryptable hop. --- crypto.go | 129 +++++++++++++++++++++++++++++++++++++++++-------- obfuscation.go | 12 +++-- 2 files changed, 119 insertions(+), 22 deletions(-) diff --git a/crypto.go b/crypto.go index 54b05a3..15d3cb5 100644 --- a/crypto.go +++ b/crypto.go @@ -106,6 +106,10 @@ type DecryptedError struct { // Message is the decrypted error message. Message []byte + + // HoldTimes is an array of hold times reported by each node on the + // error path. + HoldTimes []uint32 } // zeroHMAC is the special HMAC value that allows the final node to determine @@ -333,16 +337,20 @@ const minOnionErrorLength = minPaddedOnionErrorLength + sha256.Size // onion failure is encrypted in backward manner, starting from the node where // error have occurred. As a result, in order to decrypt the error we need get // all shared secret and apply decryption in the reverse order. A structure is -// returned that contains the decrypted error message and information on the -// sender. -func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( - *DecryptedError, error) { - - // Ensure the error message length is as expected. - if len(encryptedData) < minOnionErrorLength { - return nil, fmt.Errorf("invalid error length: "+ - "expected at least %v got %v", minOnionErrorLength, - len(encryptedData)) +// returned that contains the decrypted error message and information of the +// error sender. We also report the hold times in ms for each hop on the error +// path. +func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte, + attrData []byte) (*DecryptedError, error) { + + // Ensure the error message and attribution data length is as expected. + if len(encryptedData) < minOnionErrorLength || + len(attrData) < o.hmacsAndPayloadsLen() { + + return &DecryptedError{ + Sender: o.circuit.PaymentPath[0], + SenderIdx: 1, + }, nil } sharedSecrets, _, err := generateSharedSecrets( @@ -361,10 +369,16 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( ) copy(dummySecret[:], bytes.Repeat([]byte{1}, 32)) + // Copy the failure message data in a new variable. + failData := make([]byte, len(encryptedData)) + copy(failData, encryptedData) + + holdTimes := make([]uint32, 0) + // We'll iterate a constant amount of hops to ensure that we don't give // away an timing information pertaining to the position in the route // that the error emanated from. - for i := 0; i < NumMaxHops; i++ { + for i := range o.hopCount { var sharedSecret Hash256 // If we've already found the sender, then we'll use our dummy @@ -378,15 +392,55 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( } // With the shared secret, we'll now strip off a layer of - // encryption from the encrypted error payload. - encryptedData = onionEncrypt( - Ammag, &sharedSecret, encryptedData, + // encryption from the encrypted failure and attribution + // data. + failData = onionEncrypt(Ammag, &sharedSecret, failData) + attrData = onionEncrypt(AmmagExt, &sharedSecret, attrData) + + payloads := o.payloads(attrData) + hmacs := o.hmacs(attrData) + + // Let's calculate the HMAC we expect for the corresponding + // payloads. + position := o.hopCount - i - 1 + expectedAttrHmac := o.calculateHmac( + sharedSecret, position, failData, payloads, hmacs, + ) + + // Let's retrieve the actual HMAC from the correct position in + // the HMACs array. + actualAttrHmac := hmacs[i*o.hmacSize : (i+1)*o.hmacSize] + + // If the hmac does not match up, exit with a nil message. This + // is not done for the dummy iterations. + if !bytes.Equal(actualAttrHmac, expectedAttrHmac) && + sender == 0 && i < len(o.circuit.PaymentPath) { + + sender = i + 1 + msg = nil + } + + // Extract hold time from the payload. + holdTime := binary.BigEndian.Uint32( + payloads[:o.fixedPayloadLen], ) + if sender == 0 { + // Store hold time reported by this node. + holdTimes = append(holdTimes, holdTime) + + // Update the message. + msg = failData[sha256.Size:] + } - // 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:] + // Shift payloads and hmacs to the left to prepare for the next + // iteration. + o.shiftPayloadsLeft(payloads) + o.shiftHmacsLeft(hmacs) + + // Next, we'll need to separate the failure data, from the MAC + // itself so we can reconstruct and verify it. + expectedMac := failData[:sha256.Size] + data := failData[sha256.Size:] // With the data split, we'll now re-generate the MAC using its // specified key. @@ -410,12 +464,49 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( } return &DecryptedError{ - SenderIdx: sender, Sender: o.circuit.PaymentPath[sender-1], + SenderIdx: sender, Message: msg, + HoldTimes: holdTimes, }, nil } +// shiftPayloadsLeft shifts all payloads one position to the left, preparing +// for the next decryption iteration. +func (o *OnionErrorDecrypter) shiftPayloadsLeft(payloads []byte) { + copy(payloads, payloads[o.payloadLen():o.hopCount*o.payloadLen()]) +} + +func (o *OnionErrorDecrypter) shiftHmacsLeft(hmacs []byte) { + // Work from left to right to avoid overwriting data that is still + // needed later on in the shift operation. + srcIdx := o.hopCount + destIdx := 0 + copyLen := o.hopCount - 1 + + for range o.hopCount - 1 { + // Clear first hmac slot. This slot is for the position farthest + // away from the error source. Because we are shifting, this + // cannot be relevant. + copy(hmacs[destIdx*o.hmacSize:], o.zeroHmac) + + // The hmacs of the downstream hop become the remaining hmacs + // for the current hop. + copy( + hmacs[(destIdx+1)*o.hmacSize:], + hmacs[srcIdx*o.hmacSize:(srcIdx+copyLen)*o.hmacSize], + ) + + srcIdx += copyLen + destIdx += copyLen + 1 + copyLen-- + } + + // Clear the very last hmac slot. Because we just shifted, the most + // downstream hop can never be the error source. + copy(hmacs[destIdx*o.hmacSize:], o.zeroHmac) +} + var ( // errInitWithAttrData is returned when initial error encryption is // requested while attribution data is already present. diff --git a/obfuscation.go b/obfuscation.go index 8f14989..cb31c39 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -112,12 +112,18 @@ func (c *Circuit) Encode(w io.Writer) error { // OnionErrorDecrypter is a struct that's used to decrypt onion errors in // response to failed HTLC routing attempts according to BOLT#4. type OnionErrorDecrypter struct { + *AttrErrorStructure + circuit *Circuit } -// NewOnionErrorDecrypter creates new instance of onion decrypter. -func NewOnionErrorDecrypter(circuit *Circuit) *OnionErrorDecrypter { +// NewOnionErrorDecrypter creates new instance of onion decrypter with the +// provided circuit and attributable error structure. +func NewOnionErrorDecrypter(circuit *Circuit, + structure *AttrErrorStructure) *OnionErrorDecrypter { + return &OnionErrorDecrypter{ - circuit: circuit, + circuit: circuit, + AttrErrorStructure: structure, } } From 7ccba91a245975d36e7afc29fc5808ab77d3b990 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Mon, 19 May 2025 20:04:51 +0200 Subject: [PATCH 08/10] sphinx: update legacy onion tests Update existing onion error tests to now include the attribution data. Since this is a backwards compatible change the legacy error message should behave the same, so the asserts/vectors do not need to be updated. --- obfuscation_test.go | 55 ++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/obfuscation_test.go b/obfuscation_test.go index 0051361..831a5a0 100644 --- a/obfuscation_test.go +++ b/obfuscation_test.go @@ -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 @@ -35,29 +36,40 @@ func TestOnionFailure(t *testing.T) { } // Emulate creation of the obfuscator on node where error have occurred. - obfuscator := NewOnionErrorEncrypter(sharedSecrets[len(errorPath)-1]) + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(errorPath)-1], attributableErrorTestStructure, + ) // Emulate the situation when last hop creates the onion failure // message and send it back. - obfuscatedData := obfuscator.EncryptError(true, failureData) + legacyData, attrData, err := obfuscator.EncryptError( + true, failureData, nil, 0, + ) + require.NoError(t, err) // 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 = NewOnionErrorEncrypter(sharedSecrets[i]) - obfuscatedData = obfuscator.EncryptError(false, obfuscatedData) + obfuscator = NewOnionErrorEncrypter( + sharedSecrets[i], attributableErrorTestStructure, + ) + + legacyData, attrData, err = obfuscator.EncryptError( + false, legacyData, attrData, 1, + ) + require.NoError(t, err) } // Emulate creation of the deobfuscator on the receiving onion error side. deobfuscator := NewOnionErrorDecrypter(&Circuit{ SessionKey: sessionKey, PaymentPath: paymentPath, - }) + }, attributableErrorTestStructure) // 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) + decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) if err != nil { t.Fatalf("unable to de-obfuscate the onion failure: %v", err) } @@ -191,11 +203,13 @@ func TestOnionFailureSpecVector(t *testing.T) { t.Fatalf("unable to get specification session key: %v", err) } - var obfuscatedData []byte sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) if err != nil { t.Fatalf("Unexpected error while generating secrets: %v", err) } + + var legacyData, attrData []byte + for i, test := range onionErrorData { // Decode the shared secret and check that it matchs with // specification. @@ -207,6 +221,7 @@ func TestOnionFailureSpecVector(t *testing.T) { obfuscator := NewOnionErrorEncrypter( sharedSecrets[len(sharedSecrets)-1-i], + attributableErrorTestStructure, ) var b bytes.Buffer @@ -214,7 +229,9 @@ func TestOnionFailureSpecVector(t *testing.T) { t.Fatalf("unable to encode obfuscator: %v", err) } - obfuscator2 := NewOnionErrorEncrypter(Hash256{}) + obfuscator2 := NewOnionErrorEncrypter( + Hash256{}, attributableErrorTestStructure, + ) obfuscatorReader := bytes.NewReader(b.Bytes()) if err := obfuscator2.Decode(obfuscatorReader); err != nil { t.Fatalf("unable to decode obfuscator: %v", err) @@ -233,11 +250,17 @@ func TestOnionFailureSpecVector(t *testing.T) { if i == 0 { // Emulate the situation when last hop creates the onion failure // message and send it back. - obfuscatedData = obfuscator.EncryptError(true, failureData) + legacyData, attrData, err = obfuscator.EncryptError( + true, failureData, nil, 0, + ) + require.NoError(t, err) } else { // Emulate the situation when forward node obfuscates // the onion failure. - obfuscatedData = obfuscator.EncryptError(false, obfuscatedData) + legacyData, attrData, err = obfuscator.EncryptError( + false, legacyData, attrData, 0, + ) + require.NoError(t, err) } // Decode the obfuscated data and check that it matches the @@ -247,21 +270,23 @@ func TestOnionFailureSpecVector(t *testing.T) { t.Fatalf("unable to decode spec obfusacted "+ "data: %v", err) } - if !bytes.Equal(expectedEncryptErrordData, obfuscatedData) { + + if !bytes.Equal(expectedEncryptErrordData, legacyData) { t.Fatalf("obfuscated data not match spec: expected %x, "+ - "got %x", expectedEncryptErrordData[:], - obfuscatedData[:]) + "got %x", expectedEncryptErrordData, + legacyData) } } deobfuscator := NewOnionErrorDecrypter(&Circuit{ SessionKey: sessionKey, PaymentPath: paymentPath, - }) + }, attributableErrorTestStructure, + ) // Emulate that sender node receives the failure message and trying to // unwrap it, by applying obfuscation and checking the hmac. - decryptedError, err := deobfuscator.DecryptError(obfuscatedData) + decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) if err != nil { t.Fatalf("unable to de-obfuscate the onion failure: %v", err) } From ecc5fc88c9a680de4816b95a0008047c7be10132 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Mon, 19 May 2025 20:05:08 +0200 Subject: [PATCH 09/10] sphinx: add attributable error tests We now add the attributable error test suite, which verifies that the new encrypter/decrypter behave as expected against various attrData inputs. --- attr_error_test.go | 448 +++++++++++++++++++++++++++++++++++++++ testdata/onion-test.json | 22 +- 2 files changed, 459 insertions(+), 11 deletions(-) create mode 100644 attr_error_test.go diff --git a/attr_error_test.go b/attr_error_test.go new file mode 100644 index 0000000..4efb045 --- /dev/null +++ b/attr_error_test.go @@ -0,0 +1,448 @@ +package sphinx + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" +) + +var attributableErrorTestStructure = NewAttrErrorStructure(20, 4, 4) + +// TestAttributableOnionFailure checks the ability of sender of payment to +// decode the obfuscated onion error. +func TestAttributableOnionFailure(t *testing.T) { + t.Parallel() + + // Create numHops random sphinx paymentPath. + sessionKey, paymentPath := generateRandomPath(t) + + // 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'}, minOnionErrorLength) + sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) + require.NoError(t, err) + + // Emulate creation of the obfuscator on node where error have occurred. + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(errorPath)-1], attributableErrorTestStructure, + ) + + // Emulate the situation when last hop creates the onion failure + // message and send it back. + holdTime := uint32(42) + + var attrData, legacyData []byte + + legacyData, attrData, err = obfuscator.EncryptError( + true, failureData, attrData, holdTime, + ) + require.NoError(t, err) + + payloads := []uint32{holdTime} + + // 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 = NewOnionErrorEncrypter( + sharedSecrets[i], attributableErrorTestStructure, + ) + + holdTimeIntermediate := uint32(100 + i) + legacyData, attrData, err = obfuscator.EncryptError( + false, legacyData, attrData, holdTimeIntermediate, + ) + require.NoError(t, err) + + payloads = append([]uint32{holdTimeIntermediate}, payloads...) + } + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receive the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) + require.NoError(t, err) + + // We should understand the node from which error have been received. + require.Equal(t, + errorPath[len(errorPath)-1].SerializeCompressed(), + decryptedError.Sender.SerializeCompressed()) + + require.Equal(t, len(errorPath), decryptedError.SenderIdx) + + // Check that message have been properly de-obfuscated. + require.Equal(t, failureData, decryptedError.Message) + require.Equal(t, payloads, decryptedError.HoldTimes) +} + +// 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) { + t.Parallel() + + // Create numHops random sphinx paymentPath. + sessionKey, paymentPath := generateRandomPath(t) + + // 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'}, minOnionErrorLength) + sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) + require.NoError(t, err) + + // Emulate creation of the obfuscator on node where error have occurred. + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(errorPath)-1], attributableErrorTestStructure, + ) + + // Emulate the situation when last hop creates the onion failure + // message and send it back. + holdTime := uint32(1) + + var attrData []byte + + legacyData, attrData, err := obfuscator.EncryptError( + true, failureData, attrData, holdTime, + ) + require.NoError(t, err) + + // 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 = NewOnionErrorEncrypter( + sharedSecrets[i], attributableErrorTestStructure, + ) + + holdTime := uint32(100 + i) + legacyData, attrData, err = obfuscator.EncryptError( + false, failureData, attrData, holdTime, + ) + require.NoError(t, err) + + // Hop 1 (the second hop from the sender pov) is corrupting the + // failure message. + if i == 1 { + attrData[0] ^= 255 + } + } + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receive the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) + 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) +} + +type specHop struct { + SharedSecret string `json:"shared_secret"` + EncryptedMessage string `json:"encrypted_message"` +} + +type generatedData struct { + EncodedFailureMessage string `json:"encoded_failure_message"` + + Hops []specHop `json:"hops"` +} + +type specVector struct { + generatedData `json:"generate"` +} + +// TestOnionFailureSpecVector checks that onion error corresponds to the +// specification. +func TestAttributableFailureSpecVector(t *testing.T) { + t.Parallel() + + vectorBytes, err := os.ReadFile("testdata/onion-test.json") + require.NoError(t, err) + + var vector specVector + require.NoError(t, json.Unmarshal(vectorBytes, &vector)) + + failureData, err := hex.DecodeString( + vector.EncodedFailureMessage, + ) + require.NoError(t, err) + + paymentPath, err := getSpecPubKeys() + require.NoError(t, err) + + sessionKey, err := getSpecSessionKey() + require.NoError(t, err) + + var ( + legacyData []byte + attrData []byte + ) + + sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) + require.NoError(t, err) + + for i, test := range vector.Hops { + // Decode the shared secret and check that it matchs with + // specification. + expectedSharedSecret, err := hex.DecodeString(test.SharedSecret) + require.NoError(t, err) + + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(sharedSecrets)-1-i], + attributableErrorTestStructure, + ) + + require.Equal( + t, expectedSharedSecret, obfuscator.sharedSecret[:], + ) + + holdTime := uint32(i + 1) + + if i == 0 { + // Emulate the situation when last hop creates the onion + // failure message and send it back. + legacyData, attrData, err = obfuscator.EncryptError( + true, failureData, attrData, holdTime, + ) + require.NoError(t, err) + } else { + // Emulate the situation when forward node obfuscates + // the onion failure. + legacyData, attrData, err = obfuscator.EncryptError( + false, legacyData, attrData, holdTime, + ) + require.NoError(t, err) + } + + // Decode the obfuscated data and check that it matches the + // specification. + expectedEncryptErrorData, err := hex.DecodeString( + test.EncryptedMessage, + ) + require.NoError(t, err) + + require.Equal(t, expectedEncryptErrorData, attrData) + } + + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receives the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) + require.NoError(t, err) + + // Check that message have been properly de-obfuscated. + require.Equal(t, failureData, decryptedError.Message) + + // We should understand the node from which error have been received. + require.Equal(t, + paymentPath[len(paymentPath)-1].SerializeCompressed(), + decryptedError.Sender.SerializeCompressed(), + ) + + require.Equal(t, len(paymentPath), decryptedError.SenderIdx) +} + +// TestAttributableOnionFailureZeroesMessage checks that a garbage failure is +// attributed to the first hop. +func TestAttributableOnionFailureZeroesMessage(t *testing.T) { + t.Parallel() + + // Create numHops random sphinx paymentPath. + sessionKey, paymentPath := generateRandomPath(t) + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receive the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + obfuscatedData := make([]byte, 20000) + + decryptedError, err := deobfuscator.DecryptError(obfuscatedData, nil) + require.NoError(t, err) + + require.Equal(t, 1, decryptedError.SenderIdx) +} + +// TestAttributableOnionFailureShortMessage checks that too short failure is +// attributed to the first hop. +func TestAttributableOnionFailureShortMessage(t *testing.T) { + t.Parallel() + + // Create numHops random sphinx paymentPath. + sessionKey, paymentPath := generateRandomPath(t) + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receive the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + obfuscatedData := make([]byte, deobfuscator.hmacsAndPayloadsLen()-1) + failureMsg := bytes.Repeat([]byte{1}, minOnionErrorLength) + + decryptedError, err := deobfuscator.DecryptError(failureMsg, obfuscatedData) + require.NoError(t, err) + + require.Equal(t, 1, decryptedError.SenderIdx) +} + +func generateRandomPath(t *testing.T) (*btcec.PrivateKey, []*btcec.PublicKey) { + t.Helper() + + 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)) + + return sessionKey, paymentPath +} + +func generateHashList(values ...int) []byte { + var b bytes.Buffer + for _, v := range values { + hash := [32]byte{byte(v)} + b.Write(hash[:]) + } + + return b.Bytes() +} + +const testMaxHops = 4 + +// Generate a list of 4+3+2+1 = 10 unique hmacs. The length of this list is +// fixed for the chosen maxHops. +func createTestHmacs() []byte { + return generateHashList( + 43, 42, 41, 40, + 32, 31, 30, + 21, 20, + 10, + ) +} + +const testHmacBytes = 32 + +func TestWriteDownstreamHmacs(t *testing.T) { + t.Parallel() + + require := require.New(t) + + hmacs := createTestHmacs() + + test := func(position int, expectedValues []int) { + var b bytes.Buffer + writeDownstreamHmacs( + position, testMaxHops, hmacs, testHmacBytes, &b, + ) + + expectedHashes := generateHashList(expectedValues...) + require.Equal(expectedHashes, b.Bytes()) + } + + // Assuming the current node is in the position furthest away from the + // error source, we expect three downstream hmacs to be relevant. + test(3, []int{32, 21, 10}) + + // Assuming the current node is in positions closer to the error source, + // fewer hmacs become relevant. + test(2, []int{31, 20}) + test(1, []int{30}) + test(0, []int{}) +} + +func TestShiftHmacsRight(t *testing.T) { + t.Parallel() + + require := require.New(t) + + hmacs := createTestHmacs() + + o := NewOnionErrorEncrypter( + Hash256{}, + NewAttrErrorStructure(testMaxHops, 0, 32), + ) + o.shiftHmacsRight(hmacs) + + expectedHmacs := generateHashList( + // Previous values are zeroed out. + 0, 0, 0, 0, + + // Previous first node hmacs minus the hmac representing the + // position farthest away from the error source. + 42, 41, 40, + + // And so on for the other nodes. + 31, 30, + 20, + ) + + require.Equal(expectedHmacs, hmacs) +} + +func TestShiftHmacsLeft(t *testing.T) { + t.Parallel() + + require := require.New(t) + + hmacs := createTestHmacs() + + o := NewOnionErrorDecrypter( + nil, + NewAttrErrorStructure(testMaxHops, 0, 32), + ) + o.shiftHmacsLeft(hmacs) + + expectedHmacs := generateHashList( + // The hmacs of the second hop now become the first hop hmacs. + // The slot corresponding to the position farthest away from the + // error source remains empty. Because we are shifting, this can + // never be the position of the first hop. + 0, 32, 31, 30, + + // Continue this same scheme for the downstream hops. + 0, 21, 20, + 0, 10, + 0, + ) + + require.Equal(expectedHmacs, hmacs) +} diff --git a/testdata/onion-test.json b/testdata/onion-test.json index 5721d3e..8fe1c8f 100644 --- a/testdata/onion-test.json +++ b/testdata/onion-test.json @@ -1,39 +1,39 @@ { "comment": "A testcase for a variable length hop_payload. The third payload is 275 bytes long.", "generate": { - "encodedFailureMessage": "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "encoded_failure_message": "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "session_key": "4141414141414141414141414141414141414141414141414141414141414141", "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", "hops": [ { "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", "payload": "1202023a98040205dc06080000000000000001", - "sharedSecret": "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328", - "encryptedMessage": "d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb8cf5958e3f3f2a4365d1ec0df1d83c6a6221b5b7d1ff30156a2289a1d3ee559e7c7256bda444bb8e046f860e00b3a59a85e1e1a43de215fd5e6bf646a5deab97b1912c934e31b1cfd344764d6ca7e14ea7b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252" + "shared_secret": "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328", + "encrypted_message": "d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb8cf5958e3f3f2a4365d1ec0df1d83c6a6221b5b7d1ff30156a2289a1d3ee559e7c7256bda444bb8e046f860e00b3a59a85e1e1a43de215fd5e6bf646a5deab97b1912c934e31b1cfd344764d6ca7e14ea7b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252" }, { "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f", - "sharedSecret": "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d", - "encryptedMessage": "1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf36dee297079eb841391ccd3096da76461e314863b6412efe0ffe228d51c6097db10d3edb2e50ea679820613bfe9db11ba02920ab4c1f2a79890d997f1fc022f3ab78f0029cc6de0c90be74d55f4a99bf77a50e20f8d076fe61776190a61d2f41c408871c0279309cba3b60fcdc7efc4a0e90b47cb4a418fc78f362ecc7f15ebbce9f854c09c7be300ebc1a40a69d4c7cb7a19779b6905e82bec221a709c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb" + "shared_secret": "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d", + "encrypted_message": "1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf36dee297079eb841391ccd3096da76461e314863b6412efe0ffe228d51c6097db10d3edb2e50ea679820613bfe9db11ba02920ab4c1f2a79890d997f1fc022f3ab78f0029cc6de0c90be74d55f4a99bf77a50e20f8d076fe61776190a61d2f41c408871c0279309cba3b60fcdc7efc4a0e90b47cb4a418fc78f362ecc7f15ebbce9f854c09c7be300ebc1a40a69d4c7cb7a19779b6905e82bec221a709c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb" }, { "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", "payload": "12020230d4040204e206080000000000000003", - "sharedSecret": "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc", - "encryptedMessage": "34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dc641f68e356323d766fff0aaca5039fe7fc27038195844951a97d5a5b26698a4ca1e9cd4bca1fcca0aac5fee91b18977d2ad0e399ba159733fc98f6e96898ebc39bf0028c9c81619233bab6fad0328aa183a635fac20437fa6e00e899b2527c3697a8ab7342e42d55a679b176ab76671fcd480a9894cb897fa6af0a45b917a162bed6c491972403185df7235502f7ada65769d1bfb12d29f10e25b0d3cc08bbf6de8481ac5c04df32b4533b4f764c2aefb7333202645a629fb16e4a208e9045dc36830759c852b31dd613d8b2b10bbead1ed4eb60c85e8a4517deba5ab53e39867c83c26802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a" + "shared_secret": "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc", + "encrypted_message": "34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dc641f68e356323d766fff0aaca5039fe7fc27038195844951a97d5a5b26698a4ca1e9cd4bca1fcca0aac5fee91b18977d2ad0e399ba159733fc98f6e96898ebc39bf0028c9c81619233bab6fad0328aa183a635fac20437fa6e00e899b2527c3697a8ab7342e42d55a679b176ab76671fcd480a9894cb897fa6af0a45b917a162bed6c491972403185df7235502f7ada65769d1bfb12d29f10e25b0d3cc08bbf6de8481ac5c04df32b4533b4f764c2aefb7333202645a629fb16e4a208e9045dc36830759c852b31dd613d8b2b10bbead1ed4eb60c85e8a4517deba5ab53e39867c83c26802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a" }, { "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", "payload": "1202022710040203e806080000000000000004", - "sharedSecret": "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae", - "encryptedMessage": "74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54fef30646ae53ccf0855b61f291a81e0ec6dc0f6bf69f0ca0e5889b7e23f577ba67d2a7d6a2aa91264ab9b20630ed52f8ed56cc10a869807cd1a4c2cd802d8433fee5685d6a04edb0bff248a480b93b01904bed3bb31705d1ecb7332004290cc0cd9cc2f7907cf9db28eec02985301668f53fbc28c3e095c8f3a6cd8cab28e5e442fd9ba608b8b12e098731bbfda755393bd403c62289093b40390b2bae337fc87d2606ca028311d73a9ffbdffef56020c735ada30f54e577c6a9ec515ae2739290609503404b118d7494499ecf0457d75015bb60a16288a4959d74cf5ac5d8d6c113de39f748a418d2a7083b90c9c0a09a49149fd1f2d2cde4412e5aa2421eca6fd4f6fe6b2c362ff37d1a0608c931c7ca3b8fefcfd4c44ef9c38357a0767b14f83cb49bd1989fb3f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1" + "shared_secret": "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae", + "encrypted_message": "74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54fef30646ae53ccf0855b61f291a81e0ec6dc0f6bf69f0ca0e5889b7e23f577ba67d2a7d6a2aa91264ab9b20630ed52f8ed56cc10a869807cd1a4c2cd802d8433fee5685d6a04edb0bff248a480b93b01904bed3bb31705d1ecb7332004290cc0cd9cc2f7907cf9db28eec02985301668f53fbc28c3e095c8f3a6cd8cab28e5e442fd9ba608b8b12e098731bbfda755393bd403c62289093b40390b2bae337fc87d2606ca028311d73a9ffbdffef56020c735ada30f54e577c6a9ec515ae2739290609503404b118d7494499ecf0457d75015bb60a16288a4959d74cf5ac5d8d6c113de39f748a418d2a7083b90c9c0a09a49149fd1f2d2cde4412e5aa2421eca6fd4f6fe6b2c362ff37d1a0608c931c7ca3b8fefcfd4c44ef9c38357a0767b14f83cb49bd1989fb3f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1" }, { "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", - "sharedSecret": "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66", - "encryptedMessage": "84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad" + "shared_secret": "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66", + "encrypted_message": "84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad" } ] }, From fb96cefa58f5678795e9e207b75abcd6bfe6ae34 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Mon, 2 Jun 2025 14:47:10 +0200 Subject: [PATCH 10/10] sphinx: add strictAttribution flag for onion error decryption We now add a flag to the decryption function which controls the way we're carrying out the decryption with regards to the attribution data. This is primarily done for graceful backwards compatibility, in order to prevent nodes upgrading to attributable failures from blaming their first hop peers, which would very fast blacklist their channels and cause failed payments. As an initial step, we'd want the senders to perform the non-strict decryption, eventually switching to the strict decryption once adoption is sufficient. --- attr_error_test.go | 20 +++++-- crypto.go | 139 +++++++++++++++++++++++++++++--------------- obfuscation_test.go | 14 +++-- 3 files changed, 115 insertions(+), 58 deletions(-) diff --git a/attr_error_test.go b/attr_error_test.go index 4efb045..47c6e7a 100644 --- a/attr_error_test.go +++ b/attr_error_test.go @@ -73,7 +73,9 @@ func TestAttributableOnionFailure(t *testing.T) { // Emulate that sender node receive the failure message and trying to // unwrap it, by applying obfuscation and checking the hmac. - decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) + decryptedError, err := deobfuscator.DecryptError( + legacyData, attrData, true, + ) require.NoError(t, err) // We should understand the node from which error have been received. @@ -150,7 +152,9 @@ func TestOnionFailureCorruption(t *testing.T) { // Emulate that sender node receive the failure message and trying to // unwrap it, by applying obfuscation and checking the hmac. - decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) + decryptedError, err := deobfuscator.DecryptError( + legacyData, attrData, true, + ) require.NoError(t, err) // Assert that the second hop is correctly identified as the error @@ -254,7 +258,9 @@ func TestAttributableFailureSpecVector(t *testing.T) { // Emulate that sender node receives the failure message and trying to // unwrap it, by applying obfuscation and checking the hmac. - decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) + decryptedError, err := deobfuscator.DecryptError( + legacyData, attrData, true, + ) require.NoError(t, err) // Check that message have been properly de-obfuscated. @@ -288,7 +294,9 @@ func TestAttributableOnionFailureZeroesMessage(t *testing.T) { // unwrap it, by applying obfuscation and checking the hmac. obfuscatedData := make([]byte, 20000) - decryptedError, err := deobfuscator.DecryptError(obfuscatedData, nil) + decryptedError, err := deobfuscator.DecryptError( + obfuscatedData, nil, true, + ) require.NoError(t, err) require.Equal(t, 1, decryptedError.SenderIdx) @@ -314,7 +322,9 @@ func TestAttributableOnionFailureShortMessage(t *testing.T) { obfuscatedData := make([]byte, deobfuscator.hmacsAndPayloadsLen()-1) failureMsg := bytes.Repeat([]byte{1}, minOnionErrorLength) - decryptedError, err := deobfuscator.DecryptError(failureMsg, obfuscatedData) + decryptedError, err := deobfuscator.DecryptError( + failureMsg, obfuscatedData, true, + ) require.NoError(t, err) require.Equal(t, 1, decryptedError.SenderIdx) diff --git a/crypto.go b/crypto.go index 15d3cb5..038c1ca 100644 --- a/crypto.go +++ b/crypto.go @@ -340,17 +340,42 @@ const minOnionErrorLength = minPaddedOnionErrorLength + sha256.Size // returned that contains the decrypted error message and information of the // error sender. We also report the hold times in ms for each hop on the error // path. +// +// The strictAttribution flag controls the behavior of the decryption logic +// surrounding the presence of attribution data: +// +// - If set, then the first node with bad attribution data will be blamed +// immediately. +// +// - If unset, decryption continues optimistically until a successful error +// message decryption occurs, regardless of attribution data validity. Hold +// times are still extracted for nodes that provided valid attribution data. +// +//nolint:funlen,cyclop // Constant-time per-hop decryption keeps this long. func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte, - attrData []byte) (*DecryptedError, error) { + attrData []byte, strictAttribution bool) (*DecryptedError, error) { + + // Ensure the error message field is present and has the correct length. + if len(encryptedData) < minOnionErrorLength { + return nil, fmt.Errorf("invalid error length: "+ + "expected at least %v got %v", minOnionErrorLength, + len(encryptedData)) + } - // Ensure the error message and attribution data length is as expected. - if len(encryptedData) < minOnionErrorLength || - len(attrData) < o.hmacsAndPayloadsLen() { + validAttr := true + + // If we're decrypting with strict attribution, we need to have the + // correct attribution data present too. If strictAttribution is set + // then we immediately blame the first hop. + if len(attrData) < o.hmacsAndPayloadsLen() { + if strictAttribution { + return &DecryptedError{ + Sender: o.circuit.PaymentPath[0], + SenderIdx: 1, + }, nil + } - return &DecryptedError{ - Sender: o.circuit.PaymentPath[0], - SenderIdx: 1, - }, nil + validAttr = false } sharedSecrets, _, err := generateSharedSecrets( @@ -393,50 +418,68 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte, // With the shared secret, we'll now strip off a layer of // encryption from the encrypted failure and attribution - // data. + // data. This needs to be done before parsing the attribution + // data, as the attribution data HMACs commit to it. failData = onionEncrypt(Ammag, &sharedSecret, failData) - attrData = onionEncrypt(AmmagExt, &sharedSecret, attrData) - - payloads := o.payloads(attrData) - hmacs := o.hmacs(attrData) - // Let's calculate the HMAC we expect for the corresponding - // payloads. - position := o.hopCount - i - 1 - expectedAttrHmac := o.calculateHmac( - sharedSecret, position, failData, payloads, hmacs, - ) - - // Let's retrieve the actual HMAC from the correct position in - // the HMACs array. - actualAttrHmac := hmacs[i*o.hmacSize : (i+1)*o.hmacSize] - - // If the hmac does not match up, exit with a nil message. This - // is not done for the dummy iterations. - if !bytes.Equal(actualAttrHmac, expectedAttrHmac) && - sender == 0 && i < len(o.circuit.PaymentPath) { - - sender = i + 1 - msg = nil + // If the attribution data are valid then do another round of + // attribution data decryption. + if validAttr { //nolint:nestif + attrData = onionEncrypt( + AmmagExt, &sharedSecret, attrData, + ) + + payloads := o.payloads(attrData) + hmacs := o.hmacs(attrData) + + // Let's calculate the HMAC we expect for the + // corresponding payloads. + position := o.hopCount - i - 1 + expectedAttrHmac := o.calculateHmac( + sharedSecret, position, failData, payloads, + hmacs, + ) + + // Let's retrieve the actual HMAC from the correct + // position in the HMACs array. + actualAttrHmac := hmacs[i*o.hmacSize : (i+1)*o.hmacSize] + + // If the hmac does not match up, exit with a nil + // message. This is not done for the dummy iterations. + if !bytes.Equal(actualAttrHmac, expectedAttrHmac) && + sender == 0 && i < len(o.circuit.PaymentPath) { + + if strictAttribution { + sender = i + 1 + msg = nil + } else { + // Flag the attribution data as invalid + // from this point onwards. This will + // prevent the loop from trying to + // extract anything from the attribution + // data. + validAttr = false + } + } + + // Extract hold time from the payload. + holdTime := binary.BigEndian.Uint32( + payloads[:o.fixedPayloadLen], + ) + if sender == 0 && validAttr { + // Store hold time reported by this node. + holdTimes = append(holdTimes, holdTime) + + // Update the message. + msg = failData[sha256.Size:] + } + + // Shift payloads and hmacs to the left to prepare for + // the next iteration. + o.shiftPayloadsLeft(payloads) + o.shiftHmacsLeft(hmacs) } - // Extract hold time from the payload. - holdTime := binary.BigEndian.Uint32( - payloads[:o.fixedPayloadLen], - ) - if sender == 0 { - // Store hold time reported by this node. - holdTimes = append(holdTimes, holdTime) - - // Update the message. - msg = failData[sha256.Size:] - } - - // Shift payloads and hmacs to the left to prepare for the next - // iteration. - o.shiftPayloadsLeft(payloads) - o.shiftHmacsLeft(hmacs) - // Next, we'll need to separate the failure data, from the MAC // itself so we can reconstruct and verify it. expectedMac := failData[:sha256.Size] diff --git a/obfuscation_test.go b/obfuscation_test.go index 831a5a0..f7ca935 100644 --- a/obfuscation_test.go +++ b/obfuscation_test.go @@ -42,7 +42,7 @@ func TestOnionFailure(t *testing.T) { // Emulate the situation when last hop creates the onion failure // message and send it back. - legacyData, attrData, err := obfuscator.EncryptError( + legacyData, _, err := obfuscator.EncryptError( true, failureData, nil, 0, ) require.NoError(t, err) @@ -55,8 +55,8 @@ func TestOnionFailure(t *testing.T) { sharedSecrets[i], attributableErrorTestStructure, ) - legacyData, attrData, err = obfuscator.EncryptError( - false, legacyData, attrData, 1, + legacyData, _, err = obfuscator.EncryptError( + false, legacyData, nil, 1, ) require.NoError(t, err) } @@ -69,7 +69,9 @@ func TestOnionFailure(t *testing.T) { // Emulate that sender node receive the failure message and trying to // unwrap it, by applying obfuscation and checking the hmac. - decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) + decryptedError, err := deobfuscator.DecryptError( + legacyData, nil, false, + ) if err != nil { t.Fatalf("unable to de-obfuscate the onion failure: %v", err) } @@ -286,7 +288,9 @@ func TestOnionFailureSpecVector(t *testing.T) { // Emulate that sender node receives the failure message and trying to // unwrap it, by applying obfuscation and checking the hmac. - decryptedError, err := deobfuscator.DecryptError(legacyData, attrData) + decryptedError, err := deobfuscator.DecryptError( + legacyData, attrData, false, + ) if err != nil { t.Fatalf("unable to de-obfuscate the onion failure: %v", err) }