From 3be90396650ec21e5d07e4ed4ad0361e48b68dbc 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 ed29ef6cd690b5490fb0b8c47c0ec580042fac52 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 | 16 ++++++---------- 2 files changed, 8 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..4a9a163 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,16 @@ 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 411c1baa0873e7425072e54a3e4f0ad1fe34ee9e 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 8e16b23..423082e 100644 --- a/sphinx.go +++ b/sphinx.go @@ -546,7 +546,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 { @@ -587,7 +587,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 { @@ -780,7 +780,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 c76654df710887cfa68172dd615c65fea6a59f30 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 03d87b392557cec36e83403d279a8480015484ef 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 | 153 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 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..233d294 --- /dev/null +++ b/attr_error_structure.go @@ -0,0 +1,153 @@ +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 { + 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 := 0; j < position; j++ { + _, _ = 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 8b0e8dadc1c1a8e56f5990156d462ada91d27f82 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 | 155 +++++++++++++++++++++++++++++++++++++++++++++---- error.go | 8 ++- obfuscation.go | 10 +++- 3 files changed, 160 insertions(+), 13 deletions(-) diff --git a/crypto.go b/crypto.go index 4b7f199..2daf77b 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" + + // AMMAG_EXT is the string representation for the extended ammag key + // type. Used in cypher stream generation. + AMMAG_EXT = "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. @@ -413,17 +424,141 @@ 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, fmt.Errorf("unable to encrypt, cannot " + + "initialize error with existing attribution data") + } + + if attrData == nil { + attrData = o.initializePayload(holdTime) + } + if initial { + if len(legacyData) < minPaddedOnionErrorLength { + return nil, nil, fmt.Errorf("initial data size less "+ + "than %v", 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(AMMAG_EXT, &o.sharedSecret, attrData) + + return legacy, attrError, nil +} + +func (o *OnionErrorEncrypter) shiftHmacsRight(hmacs []byte) { + totalHmacs := (o.hopCount * (o.hopCount + 1)) / 2 + + // Work from right to left to avoid overwriting data that is still + // needed. + srcIdx := totalHmacs - 2 + destIdx := totalHmacs - 1 + + // The variable copyLen contains the number of hmacs to copy for the + // current hop. + copyLen := 1 + for i := 0; i < o.hopCount-1; i++ { + // 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 + } + + // 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 := 0; i < o.hopCount; i++ { + 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 := 0; i < o.hopCount; i++ { + position := o.hopCount - i - 1 + hmac := o.calculateHmac( + o.sharedSecret, position, message, payloads, hmacs, + ) + + copy(hmacs[i*o.hmacSize:], hmac) + } +} + +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 +} + +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) +} + +func (o *OnionErrorEncrypter) shiftPayloadsRight(payloads []byte) { + copy(payloads[o.payloadLen():], payloads) +} - return onionEncrypt(&o.sharedSecret, data) +func addPayload(payloads []byte, holdTime uint32) { + binary.BigEndian.PutUint32(payloads, holdTime) } diff --git a/error.go b/error.go index a32c999..77a5e36 100644 --- a/error.go +++ b/error.go @@ -1,6 +1,8 @@ package sphinx -import "fmt" +import ( + "fmt" +) var ( // ErrReplayedPacket is an error returned when a packet is rejected @@ -24,4 +26,8 @@ var ( // ErrLogEntryNotFound is an error returned when a packet lookup in a replay // log fails because it is missing. ErrLogEntryNotFound = fmt.Errorf("sphinx packet is not in log") + + // ErrInvalidAttrStructure is an error that signals that the provided + // attribution data have an invalid length. + ErrInvalidAttrStructure = fmt.Errorf("invalid attribution data length") ) diff --git a/obfuscation.go b/obfuscation.go index 207129d..fa0e5e0 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -9,12 +9,18 @@ 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 384312afedc5d2071e1fe7e65e2d2ee8d3b3e0c9 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 | 134 ++++++++++++++++++++++++++++++++++++++++++------- obfuscation.go | 11 ++-- 2 files changed, 123 insertions(+), 22 deletions(-) diff --git a/crypto.go b/crypto.go index 2daf77b..6b257de 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) + + hopPayloads := 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 := 0; i < o.hopCount; i++ { var sharedSecret Hash256 // If we've already found the sender, then we'll use our dummy @@ -378,15 +392,54 @@ 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(AMMAG_EXT, &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, ) - // 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:] + // 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 the payload and exit with a nil message if it is + // invalid. + holdTime := o.extractPayload(payloads) + if sender == 0 { + // Store hold time reported by this node. + hopPayloads = append(hopPayloads, 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] + data := failData[sha256.Size:] // With the data split, we'll now re-generate the MAC using its // specified key. @@ -410,12 +463,55 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( } return &DecryptedError{ - SenderIdx: sender, Sender: o.circuit.PaymentPath[sender-1], + SenderIdx: sender, Message: msg, + HoldTimes: hopPayloads, }, nil } +// extractPayload extracts the payload and payload origin information from the +// given byte slice. +func (o *OnionErrorDecrypter) extractPayload(payloadBytes []byte) uint32 { + // Extract payload. + holdTime := binary.BigEndian.Uint32(payloadBytes[0:o.payloadLen()]) + + return holdTime +} + +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 i := 0; i < o.hopCount-1; i++ { + // 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) +} + // EncryptError is used to make data obfuscation using the generated shared // secret. // diff --git a/obfuscation.go b/obfuscation.go index fa0e5e0..a43f819 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -111,12 +111,17 @@ 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 c47e0d6c3b405def8f1743654d39c544adf913d1 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 | 46 +++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/obfuscation_test.go b/obfuscation_test.go index 4a9a163..51ea49b 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. @@ -206,6 +220,7 @@ func TestOnionFailureSpecVector(t *testing.T) { } obfuscator := NewOnionErrorEncrypter( sharedSecrets[len(sharedSecrets)-1-i], + attributableErrorTestStructure, ) var b bytes.Buffer @@ -213,7 +228,7 @@ 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) @@ -232,11 +247,13 @@ 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 @@ -246,21 +263,22 @@ 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[:]) + 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 cc9493e1d1cae13982d90b0f187fcdb2be891b9a 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 | 444 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 444 insertions(+) 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..cf62583 --- /dev/null +++ b/attr_error_test.go @@ -0,0 +1,444 @@ +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) { + // 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:"sharedSecret"` + EncryptedMessage string `json:"encryptedMessage"` +} + +type generatedData struct { + EncodedFailureMessage string `json:"encodedFailureMessage"` + + 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.generatedData.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) + + // Now let's verify the attributable error fields. + require.Equal(t, decryptedError.Message, failureData) + + 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) + require.Equal(t, 1, decryptedError.SenderIdx) +} + +func generateRandomPath(t *testing.T) (*btcec.PrivateKey, []*btcec.PublicKey) { + 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) { + 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) { + 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) { + 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) +} From c8af354e4e73470dbdc9d7105775830fcb0789d7 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 | 137 +++++++++++++++++++++++++++++--------------- obfuscation_test.go | 14 +++-- 3 files changed, 114 insertions(+), 57 deletions(-) diff --git a/attr_error_test.go b/attr_error_test.go index cf62583..e48028a 100644 --- a/attr_error_test.go +++ b/attr_error_test.go @@ -69,7 +69,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. @@ -144,7 +146,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 @@ -247,7 +251,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. @@ -291,7 +297,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) @@ -317,7 +325,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 6b257de..edb53e3 100644 --- a/crypto.go +++ b/crypto.go @@ -340,17 +340,40 @@ 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. func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte, - attrData []byte) (*DecryptedError, error) { + attrData []byte, strictAttribution bool) (*DecryptedError, error) { - // Ensure the error message and attribution data length is as expected. - if len(encryptedData) < minOnionErrorLength || - len(attrData) < o.hmacsAndPayloadsLen() { + // 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)) + } - return &DecryptedError{ - Sender: o.circuit.PaymentPath[0], - SenderIdx: 1, - }, nil + 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 + } else { + validAttr = false + } } sharedSecrets, _, err := generateSharedSecrets( @@ -393,49 +416,69 @@ 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(AMMAG_EXT, &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 { + attrData = onionEncrypt( + AMMAG_EXT, &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) { + + switch strictAttribution { + case true: + sender = i + 1 + msg = nil + + case false: + // 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 the payload and exit with a nil message if it + // is invalid. + holdTime := o.extractPayload(payloads) + if sender == 0 && validAttr { + // Store hold time reported by this node. + hopPayloads = append(hopPayloads, 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 the payload and exit with a nil message if it is - // invalid. - holdTime := o.extractPayload(payloads) - if sender == 0 { - // Store hold time reported by this node. - hopPayloads = append(hopPayloads, 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 51ea49b..a76a108 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) } @@ -278,7 +280,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) }