From 606afbfccaefa74ded12f5f7763425eb5010d6b8 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 30 Jun 2022 20:08:12 +0200 Subject: [PATCH 1/4] cmd+go.mod: upgrade cli tool to use urfave lib This commit updates the cli tool to use the urfave library. This makes things easier to read and will make it easier to extend the tool in future. A `genKeys` function is also added as a helper for quickly generating key pairs. --- cmd/main.go | 293 +++++++++++++++++++++++++++++++++++----------------- go.mod | 1 + go.sum | 12 +++ 3 files changed, 211 insertions(+), 95 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 5266555..52046e7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,157 +8,260 @@ import ( "io/ioutil" "log" "os" - "strings" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/urfave/cli" ) -type OnionHopSpec struct { - Realm int `json:"realm"` +const ( + defaultSessionKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + defaultAssocData = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" +) + +// main implements a simple command line utility that can be used in order to +// either generate a fresh mix-header or decode and fully process an existing +// one given a private key. +func main() { + app := cli.NewApp() + app.Name = "sphinx-cli" + app.Commands = []cli.Command{ + { + Name: "genkeys", + Usage: "A helper function to generate a random new " + + "private-public key pair.", + Action: genKeys, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "priv", + Usage: "An optional flag to provide " + + "a private key. In this " + + "case, this command just " + + "calculates and prints the " + + "associated public key", + }, + }, + }, + { + Name: "generate", + Usage: "Build a new onion.", + Action: generate, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file", + Usage: "Path to json file containing " + + "the session key and hops " + + "data.", + Required: true, + }, + cli.StringFlag{ + Name: "assoc_data", + Usage: "The associated data to include", + Value: defaultAssocData, + }, + }, + }, + { + Name: "parse", + Usage: "Peel the onion.", + Action: parse, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "onion", + Usage: "The onion to decode. This " + + "should be set if the `file` " + + "flag is not set.", + Required: true, + }, + cli.StringFlag{ + Name: "priv", + Usage: "The private key to be used " + + "for peeling the onion.", + Required: true, + }, + cli.StringFlag{ + Name: "assocData", + Usage: "The associated data to include", + Value: defaultAssocData, + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatalln(err) + } +} + +func genKeys(cli *cli.Context) error { + var ( + priv *btcec.PrivateKey + pub *btcec.PublicKey + err error + ) + if privKeyStr := cli.String("priv"); privKeyStr != "" { + privBytes, err := hex.DecodeString(privKeyStr) + if err != nil { + return err + } + priv, pub = btcec.PrivKeyFromBytes(privBytes) + + } else { + priv, err = btcec.NewPrivateKey() + if err != nil { + return err + } + + pub = priv.PubKey() + } + + fmt.Printf("Private Key: %x\nPublic Key: %x\n", priv.Serialize(), + pub.SerializeCompressed()) + + return nil +} + +type onionSpec struct { + SessionKey string `json:"session_key"` + Hops []onionHopSpec `json:"hops"` +} + +type onionHopSpec struct { PublicKey string `json:"pubkey"` Payload string `json:"payload"` } -type OnionSpec struct { - SessionKey string `json:"session_key,omitempty"` - Hops []OnionHopSpec `json:"hops"` -} +func parseOnionSpec(spec onionSpec) (*sphinx.PaymentPath, *btcec.PrivateKey, + error) { -func parseOnionSpec(spec OnionSpec) (*sphinx.PaymentPath, *btcec.PrivateKey, error) { - var path sphinx.PaymentPath - var binSessionKey []byte var err error - + sessionKeyBytes := []byte(defaultSessionKey) if spec.SessionKey != "" { - binSessionKey, err = hex.DecodeString(spec.SessionKey) + sessionKeyBytes, err = hex.DecodeString(spec.SessionKey) if err != nil { - log.Fatalf("Unable to decode the sessionKey %v: %v\n", spec.SessionKey, err) + return nil, nil, fmt.Errorf("unable to decode the "+ + "sessionKey %v: %v\n", spec.SessionKey, err) } + } - if len(binSessionKey) != 32 { - log.Fatalf("Session key must be a 32 byte hex string: %v\n", spec.SessionKey) - } - } else { - binSessionKey = bytes.Repeat([]byte{'A'}, 32) + if len(sessionKeyBytes) != 32 { + return nil, nil, fmt.Errorf("session priv key must be 32 " + + "bytes long") } - sessionKey, _ := btcec.PrivKeyFromBytes(binSessionKey) + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) + var path sphinx.PaymentPath for i, hop := range spec.Hops { binKey, err := hex.DecodeString(hop.PublicKey) - if err != nil || len(binKey) != 33 { - log.Fatalf("%s is not a valid hex pubkey %s", hop.PublicKey, err) + if err != nil { + return nil, nil, err } pubkey, err := btcec.ParsePubKey(binKey) if err != nil { - log.Fatalf("%s is not a valid hex pubkey %s", hop.PublicKey, err) + return nil, nil, err } path[i].NodePub = *pubkey payload, err := hex.DecodeString(hop.Payload) if err != nil { - log.Fatalf("%s is not a valid hex payload %s", - hop.Payload, err) + return nil, nil, err } hopPayload, err := sphinx.NewHopPayload(nil, payload) if err != nil { - log.Fatalf("unable to make payload: %v", err) + return nil, nil, err } path[i].HopPayload = hopPayload - - fmt.Fprintf(os.Stderr, "Node %d pubkey %x\n", i, pubkey.SerializeCompressed()) } + return &path, sessionKey, nil } -// main implements a simple command line utility that can be used in order to -// either generate a fresh mix-header or decode and fully process an existing -// one given a private key. -func main() { - args := os.Args +func generate(ctx *cli.Context) error { + var spec onionSpec - assocData := bytes.Repeat([]byte{'B'}, 32) + file := ctx.String("file") + jsonSpec, err := ioutil.ReadFile(file) + if err != nil { + return fmt.Errorf("unable to read JSON onion spec from "+ + "file %v: %v", file, err) + } - if len(args) < 3 { - fmt.Printf("Usage: %s (generate|decode) \n", args[0]) - return - } else if args[1] == "generate" { - var spec OnionSpec + if err := json.Unmarshal(jsonSpec, &spec); err != nil { + log.Fatalf("Unable to parse JSON onion spec: %v", err) + } - jsonSpec, err := ioutil.ReadFile(args[2]) - if err != nil { - log.Fatalf("Unable to read JSON onion spec from file %v: %v", args[2], err) - } + path, sessionKey, err := parseOnionSpec(spec) + if err != nil { + log.Fatalf("could not parse onion spec: %v", err) + } - if err := json.Unmarshal(jsonSpec, &spec); err != nil { - log.Fatalf("Unable to parse JSON onion spec: %v", err) - } + msg, err := sphinx.NewOnionPacket( + path, sessionKey, []byte(ctx.String("assoc_data")), + sphinx.DeterministicPacketFiller, + ) + if err != nil { + log.Fatalf("Error creating message: %v", err) + } - path, sessionKey, err := parseOnionSpec(spec) - if err != nil { - log.Fatalf("could not parse onion spec: %v", err) - } + w := bytes.NewBuffer([]byte{}) + err = msg.Encode(w) + if err != nil { + log.Fatalf("Error serializing message: %v", err) + } - msg, err := sphinx.NewOnionPacket( - path, sessionKey, assocData, - sphinx.DeterministicPacketFiller, - ) - if err != nil { - log.Fatalf("Error creating message: %v", err) - } + fmt.Printf("%x\n", w.Bytes()) + return nil +} - w := bytes.NewBuffer([]byte{}) - err = msg.Encode(w) - if err != nil { - log.Fatalf("Error serializing message: %v", err) - } +func parse(ctx *cli.Context) error { + sessionKeyBytes, err := hex.DecodeString(ctx.String("priv")) + if err != nil { + return err + } - fmt.Printf("%x\n", w.Bytes()) - } else if args[1] == "decode" { - binKey, err := hex.DecodeString(args[2]) - if len(binKey) != 32 || err != nil { - log.Fatalf("Argument not a valid hex private key") - } + if len(sessionKeyBytes) != 32 { + return fmt.Errorf("session key must be 32 bytes") + } - hexBytes, _ := ioutil.ReadAll(os.Stdin) - binMsg, err := hex.DecodeString(strings.TrimSpace(string(hexBytes))) - if err != nil { - log.Fatalf("Error decoding message: %s", err) - } + sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) - privkey, _ := btcec.PrivKeyFromBytes(binKey) - privKeyECDH := &sphinx.PrivKeyECDH{PrivKey: privkey} - replayLog := sphinx.NewMemoryReplayLog() - s := sphinx.NewRouter( - privKeyECDH, &chaincfg.TestNet3Params, replayLog, - ) + onion, err := hex.DecodeString(ctx.String("onion")) + if err != nil { + return err + } - replayLog.Start() - defer replayLog.Stop() + var packet sphinx.OnionPacket + err = packet.Decode(bytes.NewBuffer(onion)) + if err != nil { + return err + } - var packet sphinx.OnionPacket - err = packet.Decode(bytes.NewBuffer(binMsg)) + s := sphinx.NewRouter( + &sphinx.PrivKeyECDH{PrivKey: sessionKey}, + &chaincfg.TestNet3Params, sphinx.NewMemoryReplayLog(), + ) + s.Start() + defer s.Stop() - if err != nil { - log.Fatalf("Error parsing message: %v", err) - } - p, err := s.ProcessOnionPacket(&packet, assocData, 10) - if err != nil { - log.Fatalf("Failed to decode message: %s", err) - } + p, err := s.ProcessOnionPacket( + &packet, []byte(ctx.String("assocData")), 10, nil, + ) + if err != nil { + return err + } - w := bytes.NewBuffer([]byte{}) - err = p.NextPacket.Encode(w) + w := bytes.NewBuffer([]byte{}) + err = p.NextPacket.Encode(w) - if err != nil { - log.Fatalf("Error serializing message: %v", err) - } - fmt.Printf("%x\n", w.Bytes()) + if err != nil { + log.Fatalf("Error serializing message: %v", err) } + fmt.Printf("%x\n", w.Bytes()) + return nil } diff --git a/go.mod b/go.mod index d274362..687cd89 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 + github.com/urfave/cli v1.22.5 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 ) diff --git a/go.sum b/go.sum index fe83050..b7e4adf 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -20,6 +21,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -53,6 +56,14 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= @@ -89,5 +100,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From ee90b334667d5798a83ef08aad843c2dd429d177 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 30 Jun 2022 20:57:57 +0200 Subject: [PATCH 2/4] blinding: add blind route and decrypt data funcs In this commit, some route blinding helper methods are added, namely `BlindBlindedRoute` which the creator of a blinded route will be able to use to construct a blinded route and `DecryptBlindedData` which a hop in the blinded route will be able to use to decrypt data encrypted for it by the constructor of the blinded route. Test vectors from the spec proposal are also added. --- blinding.go | 128 ++++++++++++++++++++ blinding_test.go | 195 ++++++++++++++++++++++++++++++ crypto.go | 29 +++++ go.mod | 1 + go.sum | 10 ++ testdata/route-blinding-test.json | 139 +++++++++++++++++++++ 6 files changed, 502 insertions(+) create mode 100644 blinding.go create mode 100644 blinding_test.go create mode 100644 testdata/route-blinding-test.json diff --git a/blinding.go b/blinding.go new file mode 100644 index 0000000..5ff9f50 --- /dev/null +++ b/blinding.go @@ -0,0 +1,128 @@ +package sphinx + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" +) + +const routeBlindingHMACKey = "blinded_node_id" + +// BlindedPath represents all the data that the creator of a blinded path must +// transmit to the builder of route that will send to this path. +type BlindedPath struct { + // IntroductionPoint is the real node ID of the first hop in the blinded + // path. The sender should be able to find this node in the network + // graph and route to it. + IntroductionPoint *btcec.PublicKey + + // BlindingPoint is the first ephemeral blinding point. This is the + // point that the introduction node will use in order to create a shared + // secret with the builder of the blinded route. This point will need + // to be communicated to the introduction point by the sender in some + // way. + BlindingPoint *btcec.PublicKey + + // BlindedHops is a list of ordered blinded node IDs representing the + // blinded route. Note that the first node ID is the blinded node ID of + // the introduction point which does not strictly need to be transmitted + // to the sender. + BlindedHops []*btcec.PublicKey + + // EncryptedData is a list of encrypted_data byte arrays. Each entry + // has been encrypted by the blinded route creator for a hop in the + // blinded route. + EncryptedData [][]byte +} + +// BlindedPathHop represents a single hop in a blinded path. It is the data that +// the blinded route creator must provide about the hop in order for the +// BlindedPath to be constructed. +type BlindedPathHop struct { + // NodePub is the real node ID of this hop. + NodePub *btcec.PublicKey + + // Payload is the cleartext blob that should be encrypted for the hop. + Payload []byte +} + +// BuildBlindedPath creates a new BlindedPath from a list of BlindedPathHops and +// a session key. +func BuildBlindedPath(sessionKey *btcec.PrivateKey, + paymentPath []*BlindedPathHop) (*BlindedPath, error) { + + if len(paymentPath) < 1 { + return nil, fmt.Errorf("at least 1 hop required to create a " + + "blinded path") + } + + bp := BlindedPath{ + IntroductionPoint: paymentPath[0].NodePub, + BlindingPoint: sessionKey.PubKey(), + EncryptedData: make([][]byte, len(paymentPath)), + } + + keys := make([]*btcec.PublicKey, len(paymentPath)) + for i, p := range paymentPath { + keys[i] = p.NodePub + } + + hopSharedSecrets, err := generateSharedSecrets(keys, sessionKey) + if err != nil { + return nil, fmt.Errorf("error generating shared secret: %v", + err) + } + + for i, hop := range paymentPath { + blindingFactorBytes := generateKey( + routeBlindingHMACKey, &hopSharedSecrets[i], + ) + + var blindingFactor btcec.ModNScalar + blindingFactor.SetBytes(&blindingFactorBytes) + + blindedNodeID := blindGroupElement(hop.NodePub, blindingFactor) + bp.BlindedHops = append(bp.BlindedHops, blindedNodeID) + + rhoKey := generateKey("rho", &hopSharedSecrets[i]) + enc, err := chacha20polyEncrypt(rhoKey[:], hop.Payload) + if err != nil { + return nil, err + } + + bp.EncryptedData[i] = enc + } + + return &bp, nil +} + +// DecryptBlindedData decrypts the data encrypted by the creator of the blinded +// route. +func DecryptBlindedData(privKey SingleKeyECDH, ephemPub *btcec.PublicKey, + encryptedData []byte) ([]byte, error) { + + ss, err := privKey.ECDH(ephemPub) + if err != nil { + return nil, err + } + + ssHash := Hash256(ss) + rho := generateKey("rho", &ssHash) + return chacha20polyDecrypt(rho[:], encryptedData) +} + +// NextEphemeral computes the next ephemeral key given the current ephemeral +// key and this node's private key. +func NextEphemeral(privKey SingleKeyECDH, + ephemPub *btcec.PublicKey) (*btcec.PublicKey, error) { + + ss, err := privKey.ECDH(ephemPub) + if err != nil { + return nil, err + } + + blindingFactor := computeBlindingFactor(ephemPub, ss[:]) + nextEphem := blindGroupElement(ephemPub, blindingFactor) + + return nextEphem, nil +} diff --git a/blinding_test.go b/blinding_test.go new file mode 100644 index 0000000..d43dd99 --- /dev/null +++ b/blinding_test.go @@ -0,0 +1,195 @@ +package sphinx + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "io/ioutil" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" +) + +const routeBlindingTestFileName = "testdata/route-blinding-test.json" + +// TestBuildBlindedRoute tests BuildBlindedRoute and DecryptBlindedData against +// the spec test vectors. +func TestBlindBlindedRoute(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := ioutil.ReadFile(routeBlindingTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // blindingJsonTestCase struct defined below. + testCase := &blindingJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + require.Len(t, testCase.Generate.Hops, 4) + + // buildPaymentPath is a helper closure used to convert hopData objects + // into BlindedPathHop objects. + buildPaymentPath := func(h []hopData) []*BlindedPathHop { + path := make([]*BlindedPathHop, len(h)) + for i, hop := range h { + nodeIDStr, _ := hex.DecodeString(hop.NodeID) + nodeID, _ := btcec.ParsePubKey(nodeIDStr) + payload, _ := hex.DecodeString(hop.EncodedTLVs) + + path[i] = &BlindedPathHop{ + NodePub: nodeID, + Payload: payload, + } + } + return path + } + + // First, Eve will build a blinded path from Dave to herself. + eveSessKey := privKeyFromString(testCase.Generate.Hops[2].SessionKey) + eveDavePath := buildPaymentPath(testCase.Generate.Hops[2:]) + pathED, err := BuildBlindedPath(eveSessKey, eveDavePath) + require.NoError(t, err) + + // At this point, Eve will give her blinded path to Bob who will then + // build his own blinded route from himself to Carol. He will then + // concatenate the two paths. Note that in his TLV for Carol, Bob will + // add the `next_blinding_override` field which he will set to the + // first blinding point in Eve's blinded route. This will indicate to + // Carol that she should use this point for the next blinding key + // instead of the next blinding key that she derives. + bobCarolPath := buildPaymentPath(testCase.Generate.Hops[:2]) + bobSessKey := privKeyFromString(testCase.Generate.Hops[0].SessionKey) + pathBC, err := BuildBlindedPath(bobSessKey, bobCarolPath) + require.NoError(t, err) + + // Construct the concatenated path. + path := &BlindedPath{ + IntroductionPoint: pathBC.IntroductionPoint, + BlindingPoint: pathBC.BlindingPoint, + BlindedHops: append(pathBC.BlindedHops, + pathED.BlindedHops...), + EncryptedData: append(pathBC.EncryptedData, + pathED.EncryptedData...), + } + + // Check that the constructed path is equal to the test vector path. + require.True(t, equalPubKeys( + testCase.Route.IntroductionNodeID, path.IntroductionPoint, + )) + require.True(t, equalPubKeys( + testCase.Route.Blinding, path.BlindingPoint, + )) + + for i, hop := range testCase.Route.Hops { + require.True(t, equalPubKeys( + hop.BlindedNodeID, path.BlindedHops[i], + )) + + data, _ := hex.DecodeString(hop.EncryptedData) + require.True(t, bytes.Equal(data, path.EncryptedData[i])) + } + + // Assert that each hop is able to decode the encrypted data meant for + // it. + for i, hop := range testCase.Unblind.Hops { + priv := privKeyFromString(hop.NodePrivKey) + ephem := pubKeyFromString(hop.EphemeralPubKey) + + data, err := DecryptBlindedData( + &PrivKeyECDH{PrivKey: priv}, ephem, + path.EncryptedData[i], + ) + require.NoError(t, err) + + decoded, _ := hex.DecodeString(hop.DecryptedData) + require.True(t, bytes.Equal(data, decoded)) + + nextEphem, err := NextEphemeral( + &PrivKeyECDH{priv}, ephem, + ) + require.NoError(t, err) + + require.True(t, equalPubKeys( + hop.NextEphemeralPubKey, nextEphem, + )) + } +} + +type blindingJsonTestCase struct { + Comment string `json:"comment"` + Generate generateData `json:"generate"` + Route routeData `json:"route"` + Unblind unBlindData `json:"unblind"` +} + +type routeData struct { + Comment string `json:"comment"` + IntroductionNodeID string `json:"introduction_node_id"` + Blinding string `json:"blinding"` + Hops []blindedHop `json:"hops"` +} + +type unBlindData struct { + Comment string `json:"comment"` + Hops []unBlindedHop `json:"hops"` +} + +type generateData struct { + Comment string `json:"comment"` + Hops []hopData `json:"hops"` +} + +type unBlindedHop struct { + Alias string `json:"alias"` + NodePrivKey string `json:"node_privkey"` + EphemeralPubKey string `json:"ephemeral_pubkey"` + DecryptedData string `json:"decrypted_data"` + NextEphemeralPubKey string `json:"next_ephemeral_pubkey"` + NextEphemeralPubKeyOverride string `json:"next_ephemeral_pubkey_override"` +} + +type hopData struct { + Comment string `json:"comment"` + SessionKey string `json:"session_key"` + Alias string `json:"alias"` + NodeID string `json:"node_id"` + Tlvs tlvs `json:"tlvs"` + EncodedTLVs string `json:"encoded_tlvs"` + EphemeralPrivKey string `json:"ephemeral_privkey"` + EphemeralPubKey string `json:"ephemeral_pubkey"` + SharedSecret string `json:"shared_secret"` + Rho string `json:"rho"` + EncryptedData string `json:"encrypted_data"` + BlindedNodeID string `json:"blinded_node_id"` +} + +type tlvs struct { + Padding string `json:"padding"` + ShortChannelID string `json:"short_channel_id"` + NextNodeID string `json:"next_node_id"` + NextBlindingOverride string `json:"next_blinding_override"` + UnknownTag65001 string `json:"unknown_tag_65001"` + UnknownTag65535 string `json:"unknown_tag_65535"` +} + +type blindedHop struct { + BlindedNodeID string `json:"blinded_node_id"` + EncryptedData string `json:"encrypted_data"` +} + +func equalPubKeys(pkStr string, pk *btcec.PublicKey) bool { + return hex.EncodeToString(pk.SerializeCompressed()) == pkStr +} + +func privKeyFromString(pkStr string) *btcec.PrivateKey { + bytes, _ := hex.DecodeString(pkStr) + key, _ := btcec.PrivKeyFromBytes(bytes) + return key +} + +func pubKeyFromString(pkStr string) *btcec.PublicKey { + bytes, _ := hex.DecodeString(pkStr) + key, _ := btcec.ParsePubKey(bytes) + return key +} diff --git a/crypto.go b/crypto.go index 939f9ec..903a736 100644 --- a/crypto.go +++ b/crypto.go @@ -10,6 +10,7 @@ import ( "github.com/aead/chacha20" "github.com/btcsuite/btcd/btcec/v2" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + "golang.org/x/crypto/chacha20poly1305" ) const ( @@ -199,6 +200,34 @@ func blindBaseElement(blindingFactor btcec.ModNScalar) *btcec.PublicKey { return priv.PubKey() } +// chacha20polyEncrypt initialises the ChaCha20Poly1305 algorithm with the given +// key and uses it to encrypt the passed message. +func chacha20polyEncrypt(key, msg []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + nonce := bytes.Repeat([]byte{0x00}, aead.NonceSize()) + + // Encrypt the message and append the ciphertext to the nonce. + return aead.Seal(nil, nonce, msg, nil), nil +} + +// chacha20polyDecrypt initialises the ChaCha20Poly1305 algorithm with the given +// key and uses it to decrypt the passed cipher text. +func chacha20polyDecrypt(key, cipherTxt []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + + nonce := bytes.Repeat([]byte{0x00}, aead.NonceSize()) + + // Decrypt the message and append the ciphertext to the nonce. + return aead.Open(nil, nonce, cipherTxt, nil) +} + // sharedSecretGenerator is an interface that abstracts away exactly *how* the // shared secret for each hop is generated. // diff --git a/go.mod b/go.mod index 687cd89..6118a0b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 + github.com/stretchr/testify v1.8.0 github.com/urfave/cli v1.22.5 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 ) diff --git a/go.sum b/go.sum index b7e4adf..40b396b 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,7 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= @@ -62,6 +63,11 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -96,6 +102,7 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -103,3 +110,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testdata/route-blinding-test.json b/testdata/route-blinding-test.json new file mode 100644 index 0000000..8b9ed57 --- /dev/null +++ b/testdata/route-blinding-test.json @@ -0,0 +1,139 @@ +{ + "comment": "test vector for using blinded routes", + "generate": { + "comment": "This section contains test data for creating a blinded route. This route is the concatenation of two blinded routes: one from Dave to Eve and one from Bob to Carol.", + "hops": [ + { + "comment": "Bob creates a Bob -> Carol route with the following session_key and concatenates it with the Dave -> Eve route.", + "session_key": "0202020202020202020202020202020202020202020202020202020202020202", + "alias": "Bob", + "node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "tlvs": { + "padding": "00000000000000000000000000000000", + "short_channel_id": "0x0x42", + "next_node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "unknown_tag_65001": "123456" + }, + "encoded_tlvs": "0110000000000000000000000000000000000208000000000000002a0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007fdfde903123456", + "ephemeral_privkey": "0202020202020202020202020202020202020202020202020202020202020202", + "ephemeral_pubkey": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "shared_secret": "76771bab0cc3d0de6e6f60147fd7c9c7249a5ced3d0612bdfaeec3b15452229d", + "rho": "ba217b23c0978d84c4a19be8a9ff64bc1b40ed0d7ecf59521567a5b3a9a1dd48", + "encrypted_data": "cd4b00ff9c09ed28102b210ac73aa12d63e90a5acebc496c49f57c639e098acbaec5b5ffb8592b07bdb6665ccb56f1258ab1857383f6542c8371dcee568a0a35a218288814849db13ce6f84a464fa517d9e1684333e3", + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25" + }, + { + "comment": "Notice the next_blinding_override tlv in Carol's payload, indicating that Bob concatenated his route with another blinded route starting at Dave.", + "alias": "Carol", + "node_id": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + "tlvs": { + "next_node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "next_blinding_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + }, + "encoded_tlvs": "0421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e6686809910821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "ephemeral_privkey": "0a2aa791ac81265c139237b2b84564f6000b1d4d0e68d4b9cc97c5536c9b61c1", + "ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", + "shared_secret": "dc91516ec6b530a3d641c01f29b36ed4dc29a74e063258278c0eeed50313d9b8", + "rho": "d1e62bae1a8e169da08e6204997b60b1a7971e0f246814c648125c35660f5416", + "encrypted_data": "ca26157e44ab01e82becf86497e1d05ad3e70903d22721210af41d791bf406873024d95b7a1ad128b2526932febfeeab237000563c1f33c78530b3880f8407326eef8bc004932b22323d13343ef740019c08e538e5c5", + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7" + }, + { + "comment": "Eve creates a Dave -> Eve blinded route using the following session_key.", + "session_key": "0101010101010101010101010101010101010101010101010101010101010101", + "alias": "Dave", + "node_id": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + "tlvs": { + "padding": "0000000000000000000000000000000000000000000000", + "short_channel_id": "0x0x561", + "next_node_id": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145" + }, + "encoded_tlvs": "0117000000000000000000000000000000000000000000000002080000000000000231042102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "ephemeral_privkey": "0101010101010101010101010101010101010101010101010101010101010101", + "ephemeral_pubkey": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "shared_secret": "dc46f3d1d99a536300f17bc0512376cc24b9502c5d30144674bfaa4b923d9057", + "rho": "393aa55d35c9e207a8f28180b81628a31dff558c84959cdc73130f8c321d6a06", + "encrypted_data": "0f94a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86563a5ee1f679ee8db3c6719bd4364f469aa5fea76ffdc49543d568a707ab73a3e855b25ca585bf12c9d5c9cb6c5c10374a4a66d95aeeea4fe146d0c2754", + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf" + }, + { + "comment": "Eve is the final recipient, so she included a path_id in her own payload to verify that the route is used when she expects it.", + "alias": "Eve", + "node_id": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "tlvs": { + "padding": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "path_id": "00112233445566778899aabbccddeeff", + "unknown_tag_65535": "06c1" + }, + "encoded_tlvs": "012c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000061000112233445566778899aabbccddeefffdffff0206c1", + "ephemeral_privkey": "62e8bcd6b5f7affe29bec4f0515aab2eebd1ce848f4746a9638aa14e3024fb1b", + "ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", + "shared_secret": "352a706b194c2b6d0a04ba1f617383fb816dc5f8f9ac0b60dd19c9ae3b517289", + "rho": "719d0307340b1c68b79865111f0de6e97b093a30bc603cebd1beb9eef116f2d8", + "encrypted_data": "da2c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63a7e4ea796de84fc9af674952e900ff518ed6b3640a7e47b5f3e4fbce5fab87e47a11d84c66d1234f1cec1da2f56b72b64896509aef9b754", + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae" + } + ] + }, + "route": { + "comment": "This section contains the resulting blinded route, which can then be used inside onion messages or payments.", + "introduction_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "blinding": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "hops": [ + { + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25", + "encrypted_data": "cd4b00ff9c09ed28102b210ac73aa12d63e90a5acebc496c49f57c639e098acbaec5b5ffb8592b07bdb6665ccb56f1258ab1857383f6542c8371dcee568a0a35a218288814849db13ce6f84a464fa517d9e1684333e3" + }, + { + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "encrypted_data": "ca26157e44ab01e82becf86497e1d05ad3e70903d22721210af41d791bf406873024d95b7a1ad128b2526932febfeeab237000563c1f33c78530b3880f8407326eef8bc004932b22323d13343ef740019c08e538e5c5" + }, + { + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "encrypted_data": "0f94a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86563a5ee1f679ee8db3c6719bd4364f469aa5fea76ffdc49543d568a707ab73a3e855b25ca585bf12c9d5c9cb6c5c10374a4a66d95aeeea4fe146d0c2754" + }, + { + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "encrypted_data": "da2c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63a7e4ea796de84fc9af674952e900ff518ed6b3640a7e47b5f3e4fbce5fab87e47a11d84c66d1234f1cec1da2f56b72b64896509aef9b754" + } + ] + }, + "unblind": { + "comment": "This section contains test data for unblinding the route at each intermediate hop.", + "hops": [ + { + "alias": "Bob", + "node_privkey": "4242424242424242424242424242424242424242424242424242424242424242", + "ephemeral_pubkey": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "blinded_privkey": "d12fec0332c3e9d224789a17ebd93595f37d37bd8ef8bd3d2e6ce50acb9e554f", + "decrypted_data": "0110000000000000000000000000000000000208000000000000002a0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007fdfde903123456", + "next_ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0" + }, + { + "alias": "Carol", + "node_privkey": "4343434343434343434343434343434343434343434343434343434343434343", + "ephemeral_pubkey": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0", + "blinded_privkey": "bfa697fbbc8bbc43ca076e6dd60d306038a32af216b9dc6fc4e59e5ae28823c1", + "decrypted_data": "0421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e6686809910821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "next_ephemeral_pubkey": "03af5ccc91851cb294e3a364ce63347709a08cdffa58c672e9a5c587ddd1bbca60", + "next_ephemeral_pubkey_override": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + }, + { + "alias": "Dave", + "node_privkey": "4444444444444444444444444444444444444444444444444444444444444444", + "ephemeral_pubkey": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "blinded_privkey": "cebc115c7fce4c295dc396dea6c79115b289b8ceeceea2ed61cf31428d88fc4e", + "decrypted_data": "0117000000000000000000000000000000000000000000000002080000000000000231042102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + "next_ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a" + }, + { + "alias": "Eve", + "node_privkey": "4545454545454545454545454545454545454545454545454545454545454545", + "ephemeral_pubkey": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a", + "blinded_privkey": "ff4e07da8d92838bedd019ce532eb990ed73b574e54a67862a1df81b40c0d2af", + "decrypted_data": "012c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000061000112233445566778899aabbccddeefffdffff0206c1", + "next_ephemeral_pubkey": "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589" + } + ] + } +} \ No newline at end of file From 2f7de78cffba845881e534b2d3b948452f0a5a39 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 30 Jun 2022 21:32:07 +0200 Subject: [PATCH 3/4] multi: handle route blinding in ProcessOnionPacket In this commit, ProcessOnionPacket is updated to take in a blinding key as a parameter. This key is then used in order to determine the blinding factor necessary to decrypt the onion. This change is accompanied with a test that tests this change against a test vector from the route blinding spec PR. --- bench_test.go | 2 +- blinding_test.go | 139 +++++++++++++++++++++++- crypto.go | 40 ++++++- obfuscation.go | 2 +- sphinx.go | 33 +++--- sphinx_test.go | 33 +++--- testdata/onion-route-blinding-test.json | 86 +++++++++++++++ 7 files changed, 299 insertions(+), 36 deletions(-) create mode 100644 testdata/onion-route-blinding-test.json diff --git a/bench_test.go b/bench_test.go index ef9e8ba..31ca80a 100644 --- a/bench_test.go +++ b/bench_test.go @@ -77,7 +77,7 @@ func BenchmarkProcessPacket(b *testing.B) { pkt *ProcessedPacket ) for i := 0; i < b.N; i++ { - pkt, err = path[0].ProcessOnionPacket(sphinxPacket, nil, uint32(i)) + pkt, err = path[0].ProcessOnionPacket(sphinxPacket, nil, uint32(i), nil) if err != nil { b.Fatalf("unable to process packet %d: %v", i, err) } diff --git a/blinding_test.go b/blinding_test.go index d43dd99..a36349b 100644 --- a/blinding_test.go +++ b/blinding_test.go @@ -8,10 +8,14 @@ import ( "testing" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" "github.com/stretchr/testify/require" ) -const routeBlindingTestFileName = "testdata/route-blinding-test.json" +const ( + routeBlindingTestFileName = "testdata/route-blinding-test.json" + onionRouteBlindingTestFileName = "testdata/onion-route-blinding-test.json" +) // TestBuildBlindedRoute tests BuildBlindedRoute and DecryptBlindedData against // the spec test vectors. @@ -116,6 +120,139 @@ func TestBlindBlindedRoute(t *testing.T) { } } +// TestOnionRouteBlinding tests that an onion packet can correctly be processed +// by a node in a blinded route. +func TestOnionRouteBlinding(t *testing.T) { + t.Parallel() + + // First, we'll read out the raw Json file at the target location. + jsonBytes, err := ioutil.ReadFile(onionRouteBlindingTestFileName) + require.NoError(t, err) + + // Once we have the raw file, we'll unpack it into our + // blindingJsonTestCase struct defined above. + testCase := &onionBlindingJsonTestCase{} + require.NoError(t, json.Unmarshal(jsonBytes, testCase)) + + assoc, err := hex.DecodeString(testCase.Generate.AssocData) + require.NoError(t, err) + + // Extract the original onion packet to be processed. + onion, err := hex.DecodeString(testCase.Generate.Onion) + require.NoError(t, err) + + onionBytes := bytes.NewReader(onion) + onionPacket := &OnionPacket{} + require.NoError(t, onionPacket.Decode(onionBytes)) + + // peelOnion is a helper closure that can be used to set up a Router + // and use it to process the given onion packet. + peelOnion := func(key *btcec.PrivateKey, onionPkt *OnionPacket, + blindingPoint *btcec.PublicKey) *ProcessedPacket { + + r := NewRouter( + &PrivKeyECDH{PrivKey: key}, &chaincfg.MainNetParams, + NewMemoryReplayLog(), + ) + + r.Start() + defer r.Stop() + + res, err := r.ProcessOnionPacket( + onionPacket, assoc, 10, blindingPoint, + ) + require.NoError(t, err) + return res + } + + hops := testCase.Decrypt.Hops + require.Len(t, hops, 5) + + // There are some things that the processor of the onion packet will + // only be able to determine from the actual contents of the encrypted + // data it receives. These things include the next_blinding_point for + // the introduction point and the next_blinding_override. The decryption + // of this data is dependent on the encoding chosen by higher layers. + // The test uses TLVs. Since the extraction of this data is dependent + // on layers outside the scope of this library, we provide handle these + // cases manually for the sake of the test. + var ( + introPointIndex = 2 + firstBlinding = pubKeyFromString(hops[1].NextBlinding) + + concatIndex = 3 + blindingOverride = pubKeyFromString(hops[2].NextBlinding) + ) + + var blindingPoint *btcec.PublicKey + for i, hop := range testCase.Decrypt.Hops { + buff := bytes.NewBuffer(nil) + require.NoError(t, onionPacket.Encode(buff)) + require.Equal(t, hop.Onion, hex.EncodeToString(buff.Bytes())) + + priv := privKeyFromString(hop.NodePrivKey) + + if i == introPointIndex { + blindingPoint = firstBlinding + } else if i == concatIndex { + blindingPoint = blindingOverride + } + + processedPkt := peelOnion(priv, onionPacket, blindingPoint) + + if blindingPoint != nil { + blindingPoint, err = NextEphemeral( + &PrivKeyECDH{priv}, blindingPoint, + ) + require.NoError(t, err) + } + onionPacket = processedPkt.NextPacket + } +} + +type onionBlindingJsonTestCase struct { + Comment string `json:"comment"` + Generate generateOnionData `json:"generate"` + Decrypt decryptData `json:"decrypt"` +} + +type generateOnionData struct { + Comment string `json:"comment"` + SessionKey string `json:"session_key"` + AssocData string `json:"associated_data"` + BlindedRoute blindedRoute `json:"blinded_route"` + FullRoute fullRoute `json:"full_route"` + Onion string `json:"onion"` +} + +type blindedRoute struct { + Comment string `json:"comment"` + IntroductionNodeID string `json:"introduction_node_id"` + Blinding string `json:"blinding"` + Hops []blindedHop `json:"hops"` +} + +type fullRoute struct { + Comment string `json:"comment"` + Hops []fullRouteHopData `json:"hops"` +} + +type fullRouteHopData struct { + PubKey string `json:"pubkey"` + Payload string `json:"payload"` +} + +type decryptData struct { + Comment string `json:"comment"` + Hops []decryptHops `json:"hops"` +} + +type decryptHops struct { + Onion string `json:"onion"` + NodePrivKey string `json:"node_privkey"` + NextBlinding string `json:"next_blinding""` +} + type blindingJsonTestCase struct { Comment string `json:"comment"` Generate generateData `json:"generate"` diff --git a/crypto.go b/crypto.go index 903a736..bfc9442 100644 --- a/crypto.go +++ b/crypto.go @@ -238,17 +238,47 @@ type sharedSecretGenerator interface { generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) } -// generateSharedSecret generates the shared secret by given ephemeral key. -func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) { +// generateSharedSecret generates the shared secret by given +// ephemeral key. If a blindingPoint is provided then it is used to tweak our +// private key before creating the shared secret with the ephemeral key. +func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey, + blindingPoint *btcec.PublicKey) (Hash256, error) { + + // If no blinding point is provided, then the un-tweaked dhKey can + // be used to derive the shared secret + if blindingPoint == nil { + return sharedSecret(r.onionKey, dhKey) + } + + // We use the blinding point to calculate the blinding factor that the + // receiver used with us so that we can use it to tweak our priv key. + // The sender would have created their shared secret with our blinded + // pub key. + ssReceiver, err := sharedSecret(r.onionKey, blindingPoint) + if err != nil { + return Hash256{}, err + } + + blindingFactorBytes := generateKey(routeBlindingHMACKey, &ssReceiver) + var blindingFactor btcec.ModNScalar + blindingFactor.SetBytes(&blindingFactorBytes) + + ephemeral := blindGroupElement(dhKey, blindingFactor) + return sharedSecret(r.onionKey, ephemeral) +} + +// sharedSecret does a ECDH operation on the passed private and public keys and +// returns the result. +func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) { var sharedSecret Hash256 // Ensure that the public key is on our curve. - if !dhKey.IsOnCurve() { + if !pub.IsOnCurve() { return sharedSecret, ErrInvalidOnionKey } - // Compute our shared secret. - return r.onionKey.ECDH(dhKey) + // Compute the shared secret. + return priv.ECDH(pub) } // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a diff --git a/obfuscation.go b/obfuscation.go index b8df7cc..42c5d04 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -18,7 +18,7 @@ type OnionErrorEncrypter struct { func NewOnionErrorEncrypter(router *Router, ephemeralKey *btcec.PublicKey) (*OnionErrorEncrypter, error) { - sharedSecret, err := router.generateSharedSecret(ephemeralKey) + sharedSecret, err := router.generateSharedSecret(ephemeralKey, nil) if err != nil { return nil, err } diff --git a/sphinx.go b/sphinx.go index 36e9a81..9b44351 100644 --- a/sphinx.go +++ b/sphinx.go @@ -530,11 +530,14 @@ func (r *Router) Stop() { // In the case of a successful packet processing, and ProcessedPacket struct is // returned which houses the newly parsed packet, along with instructions on // what to do next. -func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, - assocData []byte, incomingCltv uint32) (*ProcessedPacket, error) { +func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, + incomingCltv uint32, blindingPoint *btcec.PublicKey) (*ProcessedPacket, + error) { // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey) + sharedSecret, err := r.generateSharedSecret( + onionPkt.EphemeralKey, blindingPoint, + ) if err != nil { return nil, err } @@ -546,7 +549,7 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, // Continue to optimistically process this packet, deferring replay // protection until the end to reduce the penalty of multiple IO // operations. - packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData, r) + packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) if err != nil { return nil, err } @@ -564,16 +567,18 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, // // NOTE: This method does not do any sort of replay protection, and should only // be used to reconstruct packets that were successfully processed previously. -func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, - assocData []byte) (*ProcessedPacket, error) { +func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte, + blindingPoint *btcec.PublicKey) (*ProcessedPacket, error) { // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey) + sharedSecret, err := r.generateSharedSecret( + onionPkt.EphemeralKey, blindingPoint, + ) if err != nil { return nil, err } - return processOnionPacket(onionPkt, &sharedSecret, assocData, r) + return processOnionPacket(onionPkt, &sharedSecret, assocData) } // unwrapPacket wraps a layer of the passed onion packet using the specified @@ -640,8 +645,7 @@ func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256, // packets. The processed packets returned from this method should only be used // if the packet was not flagged as a replayed packet. func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256, - assocData []byte, - sharedSecretGen sharedSecretGenerator) (*ProcessedPacket, error) { + assocData []byte) (*ProcessedPacket, error) { // First, we'll unwrap an initial layer of the onion packet. Typically, // we'll only have a single layer to unwrap, However, if the sender has @@ -728,11 +732,12 @@ func (r *Router) BeginTxn(id []byte, nels int) *Tx { // returned which houses the newly parsed packet, along with instructions on // what to do next. func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, - assocData []byte, incomingCltv uint32) error { + assocData []byte, incomingCltv uint32, + blindingPoint *btcec.PublicKey) error { // Compute the shared secret for this onion packet. sharedSecret, err := t.router.generateSharedSecret( - onionPkt.EphemeralKey, + onionPkt.EphemeralKey, blindingPoint, ) if err != nil { return err @@ -745,9 +750,7 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, // Continue to optimistically process this packet, deferring replay // protection until the end to reduce the penalty of multiple IO // operations. - packet, err := processOnionPacket( - onionPkt, &sharedSecret, assocData, t.router, - ) + packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData) if err != nil { return err } diff --git a/sphinx_test.go b/sphinx_test.go index 3425e76..8feedf9 100644 --- a/sphinx_test.go +++ b/sphinx_test.go @@ -180,7 +180,9 @@ func TestSphinxCorrectness(t *testing.T) { hop := nodes[i] t.Logf("Processing at hop: %v \n", i) - onionPacket, err := hop.ProcessOnionPacket(fwdMsg, nil, uint32(i)+1) + onionPacket, err := hop.ProcessOnionPacket( + fwdMsg, nil, uint32(i)+1, nil, + ) if err != nil { t.Fatalf("Node %v was unable to process the "+ "forwarding message: %v", i, err) @@ -242,7 +244,7 @@ func TestSphinxSingleHop(t *testing.T) { // Simulating a direct single-hop payment, send the sphinx packet to // the destination node, making it process the packet fully. - processedPacket, err := nodes[0].ProcessOnionPacket(fwdMsg, nil, 1) + processedPacket, err := nodes[0].ProcessOnionPacket(fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } @@ -269,14 +271,17 @@ func TestSphinxNodeRelpay(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if _, err := nodes[0].ProcessOnionPacket(fwdMsg, nil, 1); err != nil { + _, err = nodes[0].ProcessOnionPacket(fwdMsg, nil, 1, nil) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } // Now, force the node to process the packet a second time, this should // fail with a detected replay error. - if _, err := nodes[0].ProcessOnionPacket(fwdMsg, nil, 1); err != ErrReplayedPacket { - t.Fatalf("sphinx packet replay should be rejected, instead error is %v", err) + _, err = nodes[0].ProcessOnionPacket(fwdMsg, nil, 1, nil) + if err != ErrReplayedPacket { + t.Fatalf("sphinx packet replay should be rejected, instead "+ + "error is %v", err) } } @@ -296,14 +301,14 @@ func TestSphinxNodeRelpaySameBatch(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(0, fwdMsg, nil, 1); err != nil { + if err := tx.ProcessOnionPacket(0, fwdMsg, nil, 1, nil); err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } // Now, force the node to process the packet a second time, this call // should not fail, even though the batch has internally recorded this // as a duplicate. - err = tx.ProcessOnionPacket(1, fwdMsg, nil, 1) + err = tx.ProcessOnionPacket(1, fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("adding duplicate sphinx packet to batch should not "+ "result in an error, instead got: %v", err) @@ -342,7 +347,8 @@ func TestSphinxNodeRelpayLaterBatch(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1); err != nil { + err = tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } @@ -355,7 +361,7 @@ func TestSphinxNodeRelpayLaterBatch(t *testing.T) { // Now, force the node to process the packet a second time, this should // fail with a detected replay error. - err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1) + err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("sphinx packet replay should not have been rejected, "+ "instead error is %v", err) @@ -387,7 +393,8 @@ func TestSphinxNodeReplayBatchIdempotency(t *testing.T) { // Allow the node to process the initial packet, this should proceed // without any failures. - if err := tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1); err != nil { + err = tx.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) + if err != nil { t.Fatalf("unable to process sphinx packet: %v", err) } @@ -400,7 +407,7 @@ func TestSphinxNodeReplayBatchIdempotency(t *testing.T) { // Now, force the node to process the packet a second time, this should // not fail with a detected replay error. - err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1) + err = tx2.ProcessOnionPacket(uint16(0), fwdMsg, nil, 1, nil) if err != nil { t.Fatalf("sphinx packet replay should not have been rejected, "+ "instead error is %v", err) @@ -434,7 +441,7 @@ func TestSphinxAssocData(t *testing.T) { nodes[0].log.Start() defer nodes[0].log.Stop() - _, err = nodes[0].ProcessOnionPacket(fwdMsg, []byte("somethingelse"), 1) + _, err = nodes[0].ProcessOnionPacket(fwdMsg, []byte("somethingelse"), 1, nil) if err == nil { t.Fatalf("we should fail when associated data changes") } @@ -682,7 +689,7 @@ func TestSphinxHopVariableSizedPayloads(t *testing.T) { // all the layers and pass them on to the next node // properly. processedPacket, err := currentHop.ProcessOnionPacket( - nextPkt, nil, uint32(i), + nextPkt, nil, uint32(i), nil, ) if err != nil { t.Fatalf("#%v: unable to process packet at "+ diff --git a/testdata/onion-route-blinding-test.json b/testdata/onion-route-blinding-test.json new file mode 100644 index 0000000..34305e8 --- /dev/null +++ b/testdata/onion-route-blinding-test.json @@ -0,0 +1,86 @@ +{ + "comment": "test vector for a payment onion sent to a partially blinded route", + "generate": { + "comment": "This sections contains test data for creating a payment onion that sends to the provided blinded route.", + "session_key": "0202020202020202020202020202020202020202020202020202020202020202", + "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", + "blinded_route": { + "comment": "This section contains a blinded route that the sender will use for his payment, usually obtained from a Bolt 11 invoice.", + "introduction_node_id": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "blinding": "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", + "hops": [ + { + "blinded_node_id": "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25", + "encrypted_data": "cd4b00ff9c09ed28102b210ac73aa12d63e90a5acebc496c49f57c639e098acbaec5b5ffb8592b07bdb6665ccb56f1258ab1857383f6542c8371dcee568a0a35a218288814849db13ce6f84a464fa517d9e1684333e3" + }, + { + "blinded_node_id": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "encrypted_data": "ca26157e44ab01e82becf86497e1d05ad3e70903d22721210af41d791bf406873024d95b7a1ad128b2526932febfeeab237000563c1f33c78530b3880f8407326eef8bc004932b22323d13343ef740019c08e538e5c5" + }, + { + "blinded_node_id": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "encrypted_data": "0f94a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86563a5ee1f679ee8db3c6719bd4364f469aa5fea76ffdc49543d568a707ab73a3e855b25ca585bf12c9d5c9cb6c5c10374a4a66d95aeeea4fe146d0c2754" + }, + { + "blinded_node_id": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "encrypted_data": "da2c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63a7e4ea796de84fc9af674952e900ff518ed6b3640a7e47b5f3e4fbce5fab87e47a11d84c66d1234f1cec1da2f56b72b64896509aef9b754" + } + ] + }, + "full_route": { + "comment": "The sender adds one normal hops before the blinded route. It provides the blinding point to the first node in the blinded route, and encrypted_data to each node in the blinded route.", + "hops": [ + { + "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + "payload": "12020201f4040203e80608000000000000000a" + }, + { + "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + "payload": "83020201900402035c0a56cd4b00ff9c09ed28102b210ac73aa12d63e90a5acebc496c49f57c639e098acbaec5b5ffb8592b07bdb6665ccb56f1258ab1857383f6542c8371dcee568a0a35a218288814849db13ce6f84a464fa517d9e1684333e30c21024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766" + }, + { + "pubkey": "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7", + "payload": "5f0201fa040202ee0a56ca26157e44ab01e82becf86497e1d05ad3e70903d22721210af41d791bf406873024d95b7a1ad128b2526932febfeeab237000563c1f33c78530b3880f8407326eef8bc004932b22323d13343ef740019c08e538e5c5" + }, + { + "pubkey": "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf", + "payload": "5f0201c8040202bc0a560f94a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86563a5ee1f679ee8db3c6719bd4364f469aa5fea76ffdc49543d568a707ab73a3e855b25ca585bf12c9d5c9cb6c5c10374a4a66d95aeeea4fe146d0c2754" + }, + { + "pubkey": "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae", + "payload": "5f0201c8040202bc0a56da2c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63a7e4ea796de84fc9af674952e900ff518ed6b3640a7e47b5f3e4fbce5fab87e47a11d84c66d1234f1cec1da2f56b72b64896509aef9b754" + } + ] + }, + "onion": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337dcdf60020cefad5167fd8aefc5edf9391e21ad8c75ea28bcd76065fd14f9aca6c352465fa5ec1d94bc7777c2c2077d3a7fa96b954d51f3bcd335fd1b3b67858455edabd924a8628daabe92f2fc090b480d3988d8cbd1bd11a1bc3fae3a6fefb5be8e17302406461c7346f342db761555fc50f1379079c48e67f06e5c581b56a692da4148046eac7fc9774fc333d1b0792d7959a10c18e5bade7fb494a6f62e5ba25cd9af3ca5f3b3bf8c23a8c77c5e83304b1ab23eaae471f6ed9ec373a81ae375ec9165ddff1f4c20248ce7d2d2a2c68f989bbb44dfa3ac2f85a8383be5bc52e2e6d2dc68b8cb74b45a6d9b93f28cc82536cf6cabb5292793227e64b5f8a59e60cbefa153f3d59b597b1f311f89d72a15d32248be9f9c0a101658f83305ea4fbfb3cc2a74135f43b82a1afe07f51c90eef23accfd7c431dbb67959c5868d0e281a4a0fba6fa42dbcdea34138ab3282f12f24dd0e78f9bfbc2f5ca0b14d4acf9aaab3fca6b57003259ea0bd47ca8ce8f28cdcefb92a543997e29f3f6a7740d5f901f8f5a25dcbe111c68ade86bace03d942e53f8f627f602c7c0bb012267924de6a901c9e4077f65d9c0ed50fdd301e7fdae37fc5dfe40132a26ddf131fc10fc914021623228c53fcf11526db7ccc086c3a804c68d7c0065a47717c79d9324e8c95a66e3b7e9a81d837ca90fd35f043838f82ea0bbd8921ba1de5e285c3f7abbe86edb8b43bf1c2e5da395913c61899166b2bdd15888c54f1a95ac6e1c5ad428235759d482ca20c3b43b5db3895b82ec62aa70506b7867bae6c8d1250dbc53ce2abe35b7304d6f13e7cf9ce3200b2281dac26caf375b475be1e085968899e4c8f2bfdbefa348411faa4bd521c46d148d71b9e62c653316a1d5fdeca0daee3a518ff10ecbc45aeb5c318d806d9a7c92e6ad4c9f471d32f95183067df2a94bf19653d463af2ffc3f85c3ab5e2a22256c9dae919b44fa2ad3a9340d84ffdbaf3fb34d29260132ac932712a4d2c97a5aa50607d39bcb519812d85cae10a5f9c8353e3d126d36b041b30f2abc924e4e8d3859c4f46d3f6493f54d4fc33b884a83838a474c20e0589f2c856af950b21b2bdd2f4c52d53e740455023825efef9fc6eb57b112e2dbc705a2ae5b6a2e04383e03ee2ee01ba14dfd79ff9ec38144757766e249ffb718106577b02b70d7943f6d4e48c3631ca3fa8cd51b5ae1caa4dff523727fc23d4a92119d0ba597ee82a69d0abaf81c0124329b57289fb1091518cb7656ee821812c3522ad71da9c4b23a43fed1b8420ef103595c1cd34fb2c6db1e2fcbee67b8b4fd11e79cf949ac4877507c4356e5e36d17660cef0d3f40911e0f4262b632a94bd401a317923f42101c2679401400cc4059c9a853c976c785b28a7777a3c5420be5af12a416bdb3706ab6490109077644cd337a07f3a3ffa7920014e524d8dbef5e9cbb98314bfc084b2c896cfb2d1dac785e4630736ab6dceaccb0f25a3937ab2e707eeea6f54c4e91f949c043721ae7e9e4bb38ef68ba205bcf0414001519eb5797449f91d10d917ec96f64ae2ca1d7cd4eb9cf6a2436ddbc7030c4b5737b34d5cb801c904627fa5d568bf7ddb3eacb4cbebdd9295d4210c29cc5493fce9e4b3070248b34201d06045ffb3afcfdc8d58d1b16d6aa36fd8e983c135307c122d2c9421b999405aea0ebe12590ebb33bb336023d5d290dd7f83793447877d0b6689a4cf2bdc59a1b46d6aa45447f3270771fb887c328056f3c4267222ffb1e643f39a7982b96195ed2ee116df24b632a80412fa034cf48a02b12814420b9f1485dcd8d071461caab4fa3d7a879f17e14b7fcba25eed7eb6950f5f05c9e56e64720fdbbf8db62c0c1d164275a15a7aab575" + }, + "decrypt": { + "comment": "This section contains the internal values generated by intermediate nodes when decrypting their payload.", + "hops": [ + { + "onion": "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337dcdf60020cefad5167fd8aefc5edf9391e21ad8c75ea28bcd76065fd14f9aca6c352465fa5ec1d94bc7777c2c2077d3a7fa96b954d51f3bcd335fd1b3b67858455edabd924a8628daabe92f2fc090b480d3988d8cbd1bd11a1bc3fae3a6fefb5be8e17302406461c7346f342db761555fc50f1379079c48e67f06e5c581b56a692da4148046eac7fc9774fc333d1b0792d7959a10c18e5bade7fb494a6f62e5ba25cd9af3ca5f3b3bf8c23a8c77c5e83304b1ab23eaae471f6ed9ec373a81ae375ec9165ddff1f4c20248ce7d2d2a2c68f989bbb44dfa3ac2f85a8383be5bc52e2e6d2dc68b8cb74b45a6d9b93f28cc82536cf6cabb5292793227e64b5f8a59e60cbefa153f3d59b597b1f311f89d72a15d32248be9f9c0a101658f83305ea4fbfb3cc2a74135f43b82a1afe07f51c90eef23accfd7c431dbb67959c5868d0e281a4a0fba6fa42dbcdea34138ab3282f12f24dd0e78f9bfbc2f5ca0b14d4acf9aaab3fca6b57003259ea0bd47ca8ce8f28cdcefb92a543997e29f3f6a7740d5f901f8f5a25dcbe111c68ade86bace03d942e53f8f627f602c7c0bb012267924de6a901c9e4077f65d9c0ed50fdd301e7fdae37fc5dfe40132a26ddf131fc10fc914021623228c53fcf11526db7ccc086c3a804c68d7c0065a47717c79d9324e8c95a66e3b7e9a81d837ca90fd35f043838f82ea0bbd8921ba1de5e285c3f7abbe86edb8b43bf1c2e5da395913c61899166b2bdd15888c54f1a95ac6e1c5ad428235759d482ca20c3b43b5db3895b82ec62aa70506b7867bae6c8d1250dbc53ce2abe35b7304d6f13e7cf9ce3200b2281dac26caf375b475be1e085968899e4c8f2bfdbefa348411faa4bd521c46d148d71b9e62c653316a1d5fdeca0daee3a518ff10ecbc45aeb5c318d806d9a7c92e6ad4c9f471d32f95183067df2a94bf19653d463af2ffc3f85c3ab5e2a22256c9dae919b44fa2ad3a9340d84ffdbaf3fb34d29260132ac932712a4d2c97a5aa50607d39bcb519812d85cae10a5f9c8353e3d126d36b041b30f2abc924e4e8d3859c4f46d3f6493f54d4fc33b884a83838a474c20e0589f2c856af950b21b2bdd2f4c52d53e740455023825efef9fc6eb57b112e2dbc705a2ae5b6a2e04383e03ee2ee01ba14dfd79ff9ec38144757766e249ffb718106577b02b70d7943f6d4e48c3631ca3fa8cd51b5ae1caa4dff523727fc23d4a92119d0ba597ee82a69d0abaf81c0124329b57289fb1091518cb7656ee821812c3522ad71da9c4b23a43fed1b8420ef103595c1cd34fb2c6db1e2fcbee67b8b4fd11e79cf949ac4877507c4356e5e36d17660cef0d3f40911e0f4262b632a94bd401a317923f42101c2679401400cc4059c9a853c976c785b28a7777a3c5420be5af12a416bdb3706ab6490109077644cd337a07f3a3ffa7920014e524d8dbef5e9cbb98314bfc084b2c896cfb2d1dac785e4630736ab6dceaccb0f25a3937ab2e707eeea6f54c4e91f949c043721ae7e9e4bb38ef68ba205bcf0414001519eb5797449f91d10d917ec96f64ae2ca1d7cd4eb9cf6a2436ddbc7030c4b5737b34d5cb801c904627fa5d568bf7ddb3eacb4cbebdd9295d4210c29cc5493fce9e4b3070248b34201d06045ffb3afcfdc8d58d1b16d6aa36fd8e983c135307c122d2c9421b999405aea0ebe12590ebb33bb336023d5d290dd7f83793447877d0b6689a4cf2bdc59a1b46d6aa45447f3270771fb887c328056f3c4267222ffb1e643f39a7982b96195ed2ee116df24b632a80412fa034cf48a02b12814420b9f1485dcd8d071461caab4fa3d7a879f17e14b7fcba25eed7eb6950f5f05c9e56e64720fdbbf8db62c0c1d164275a15a7aab575", + "node_privkey": "4141414141414141414141414141414141414141414141414141414141414141" + }, + { + "onion": "000280caa47c2a0ea677f6a77529e46caa04212153a8d5f829bee1e7339b17e2e2a9544e50dcac27993baaf54a968b4c0c34838f8bb7bf37cd64591de74f0579dff179c31b754b310ab19bd994ff5d473ae8ac111ee153bd828d44081d0e4ebb1842f079565f7efa9d11e9fdf47901abae4b000f6a8c9c0129f274be7c2d7c7c83468d5dce3b3f35ee98bc6387c95ede06d05eec806c8ff42ac63c16779baef0d6311e0af411224745756291411af8f2b2ecdb7e59b0c09232ddd478596c091e01b668f3e010438ace38e6a95076bbedc0aaa27a866b57154412d60643912c3e0f00ab96750362e62c4959ad25d668eb2199dc6ee875e8080160767e9e9c2e26520870599962c4457056decee774d1fbc92fa2ccf8cced1b7aec05ea5c7a7d3553b8ac9eb5931047203375cd79d0cb067cf1dda28115a7ba5983afd04f055172eea987e221d06d5b6803353f180d8213d2b098341593c9651f36b5b2be62f5efe08832577319a7465c5b659fad35ee195df82c91b77ec53981ce1455c7dae5330b286dca0dd7af5cfa4502fdca838d3f568b6230fe543204960503ae584e49b4d92ed9ae61938511d33bce7e861cc66ba267a6ab5d7a31aebc0a761df76fa9332bb9adc16defc6a6587307fe762c1b8242f564ed13ce22be0be38ef1893111acc34a3c0fcfa715163a9bbd162f97ce86d35b2dd0d39e848c9f90622763e55b0edf45670a3ccb452473654168c3a90932a2b05bd7417d56c881b74b1a669e48d8e97d38db25f249f56352f3441e70e6c9dcda6dde673b9e10c1ce551572c374cd5236ac74cdb3bfe2ab94f1f36c7e5e217cbd082515e8f067ed404f22bb5cb7b05d8920ebda351ad085fe2c3e4e872296fabc06e8a463c90ad8a79c069f5b972a42b6887a038768960aac5046bcd44217eb622215c043182320bcf9b66b2099fdfe468bc26c01ce12e3bdf669a9d597d40bfc858a2456635ca476bf496839c16e6e597f7346370d4fa71378c58dce9f4527bca85f0777e9e953a520560c6334b90e54cb2256bfc3748697a858d3371ba62b6366fb19c80a8e40a4101b113b104c859b9356fb2226cf6ac21d576f524b9a922abc6c1bb0a2b19f4495cbb5eb2b8eeaa3617f931f85eb8913cfdb5862e506d1391b3097baaadef8be56b385cdbabd49dbb3cd5f1a208a1a8f26b8a49abd7fc00cba5042a4d19e764a5732b34d0f5c546c6b8eee777262b19edf8943843745a99784ffebb9c872e9c528559b9fe00dd9c959ed42495e7510bb5b6994b8d95ee5896b5808bc7fc6e27b4c486af4b68309b421bc6070489e080bd0e06838aa30545c12f8ce4e583adcb18a291a02aef42de56965cddefe087696477b7017494ae17952b00f68b68ff00061134f18bf249ac904024cbf65d3bda768a2ff03040037850c9aaa1aaed53d9b0b6d94f5d3e1ced369136a133e2989fec04d40f0b69ee295427b52966b77d251e9fcf91bb635e52a6c339b4a5ef7d6dedc46144e6a98e6b9fe8cf6a122b139ae1944b437604df0459975ae32e9ca612e9e86872b77301c3a56162350883cd1e32a775ae866a49bd18a36ccec7e51d70bc762f888bfa486492451cc547e154a9ba26234c176c9c5d030dc14972ff89e83d894166c1db6989fb4985bc7774201e894340fcf646622bde37547b84217d8bb3cdd3710c88851a0d86ef09ebd4b7981feb3baeb5a9e1368334aa4bc6c13a41292fc2674581e62bb537e5c9661c90b9bf612cffbfd143aacc5c6737e7aac7845643a56d581bbd1c4d34a0983d2810bd43a9bdc15db282410cd5dfc700f5f7a1b2af0d6a6e9d9ffc570a6d3209614ab4dc43728f3f0cd7eb4ce36ccd98936bbcbd32627384434bd01e9c0f93b91d23e88f4562e404c8264b80c38f94b177e22a144a3c721f14d9b5a36132cc9", + "node_privkey": "4242424242424242424242424242424242424242424242424242424242424242", + "next_blinding": "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0" + }, + { + "onion": "000288b48876fb0dc0d7375233ccaf2910dc0dc81ba52e5a7906f00d75e0d58dbd4bb9ca3f7e7b46c4b1aef17878bcdfba9e6eb6a361b5fce67410c944b3b544334499152c7a6c01e7adb7770a5975b5119e0f124fe788f99d67617b122cbbddb49867d0c65d3db96401ab697fba3c20c775b58233806b7bd71ec59cb9f9428b7eca14b93f7c362af6904e6e7ae41ca437380e58eef6fd3b75802bb8903d7258f4fad36a778450b05d4db760eb35fff5b3ffa55bc6cd103e3583862e06efe706b7e22e882d7f75c46069732e35e4d97dacefc40bf61665843ed7989958911813d42deaa04cfae04b83e96919b374d10f04678b158691841a875d1251008394f41040f0bb15d21079b51b7908986eba3a330fa6290fe64f184206ae5e657ff65fb9cac4afa5a3289ed84ad0b18a22a3a37d9eb6aa6abc46d40da939d08817afa2bb8ac337daa7df0cce06675875b58efe9471fe6fac0c5ba5cd8e1b516a4d88307b8a6613c82f4e329f08544a1c5932e2c0f6cbcadbda012af4126c2623e20c580038f35bdf19d1f47e075119b4a23b70fb8bb317262b463a5db294c6537fdf9a02c3ba75a90868bbe7ed670e55698dab057110c1768b56ce13c53cf07bc325857bedce9ccb8e2ffad46c0a3da4bb54a6b4daaa2b9c8c404c9d858b6ad11dd09c6d8ff780a48f3ef4913f743736393f4ef73c50ac6a4d8317775ff6420c7ca389b5a7f303ad419fc2e10340457f57bf808fc153ccdb12edca4d6a9b84e15b3167f474540f51173e1fa320e3944c45f3fcda01c027eb1984d2b03318c45d1799b69fc0df437b0bd2725324274e6200d4ed3015623df18f755214e91b8c08cb8ef3bd49b2f50080b2e3852ee616b2aa975493a24378fe737403f667342c4131a1a6d71ac5c1f9a026886a0eb8cc6c0bed63c613ae9745ab378ef5a48b3a86b281a2028b86d19dfd398bfa4273c0196cf61659a5b4b4fbdf22a0460768ad1fcc97468a56057d59ffc9d0ea459e531395cf50727e7344b32033c655b9dea7194af04642033a2a6bd9def7085f9d30ff6ce6889fe46eab55cd749591c23d55a4fd975747c8660d2019495af2bc29b3b527875110ac2830b545d760b3e9ad21df7aa8482a59a4ca43fdd79c2cc859cbe1a2e7eb34f7dc9b960ea6ec569da1b769469e5a979264ccacdaf5dbdf6d0809f8028cddca3082198bef23571c845240e59c5979238a06efb5641069a0ef1d05cd4a2a4b237b029b0ca1d69c93263388c049e9308d92955d684caa3b253abad6262977464d10ba252a6c163c6aa2c83b991c90c0de67363d42578c8cc8195715e4076984b8078e4355c2ec0c1c35dc2cde9429fdd1cdb5c7523c7268c5d72195fa7696bea1c1286978ad93aac9ceacc71a1854f7fa0e502a6416647b02205ca2062847b993d7b3882e68a1aa564ec70ce344a0b97037f4037bd69d559a96746dd9838a65fb5a37fd2bc52ca3e6513f54befd0c3434c2a742418b3c359e54e6029c1e52ab43193d9aad264ec3e30a1fccce3bd617c11f13e5f062d8a2e1727b1481b8b313716ff5029b671e9ff86b537565e5435f277aef56849c8ad693cf7f8f0b6d9ff2c89c114fa59fbac5878c22a983dfa3f5766765aefce97ea97295800fe3c7bb3205e0dd2075d79a5cc0f779845ee8b98951be61fd293d6c15b9d4653935bf17cf50bd31f8b79e60dba0e7fd6864754fd94262485a4f65e7eb3e1922f51b1a4dd2b4fd2c20d94d1213fbe90bd603dfc7e15176382e3ce0f43f980d44d23bf3c57f54a15f42c171a8f2511e28ac178c6f01396e50397a57ffb09c5e6c315bd3ae7983577c1a0386c6d5d9a2223438e321b0fedfdee58fa452c736de93586f1d150f4054de5cbbeb6a87359ed629ff434c1e54eacd8a569a650ca73380a7c6c0a4fac6abd8c673c3", + "node_privkey": "4343434343434343434343434343434343434343434343434343434343434343", + "next_blinding": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + }, + { + "onion": "0003f25471c0f2ff549a7fd7859100306bb6c294c209f34c77f782897f184b967c4980f46aac7e2b28495c2e549f4b851c29af5a79f9f94a0d58f56c46b7f5881d26f13dff9c5d88088579ccff42d170285d4b7bd665dd1e63406f5a76215216e939482c801165f92952684c7b53375609834d74d17f849b5bc2aa021213b4fb6fe74c9851e6797e51672dbc4b0f8106bf7187860fce09b0f9c62c0e711d7c0d7dd5b6556c4bd3c335e215f5507620c849cdb90df650cf38f612570f41924c79515e1b0242faac893f93e8b89b95f7016851aa2e5730f42a998d5cf6d95fb61c89a1b18762c521cde80fa8cf70a427337babf3bbcd099bb62be9092e9f7b3d1790ed6c02816eb6c2f283e18052e030c587654e2371b1091584ebf4df04df11e00e4782c89d81ccb3a2bec37bc5a69b133484911f3ba330cc22dfc8896cabe021fbe60d9f7b2680a666640ebc30d11b68e0739edcb340643524bf844c9a25a6f832c43eeaf34fcedb292d2a1c9cc20c35c0c1faf3cc5aa0ade4dbeab965a99dfe02a69284bc4c79a587a47da79a7414ece348a1574200590926a86c32ae5d25ca3b32b3e91aec2df0370c9a83fbe87ed5972b7cbdf042c6e1ceda06f7143788e16d2b5d15cef036a82779dceb24246bf5573ca4e14ca8b78f8fcb52d791c175932bc7fc612f1f63c1d31b15f36a1a41ad98139e3544135bace8faddc0f689f89fe4f707c051f7acda7cdc0f463fa98e35197b23e901e0b161a118a2d212a97ccfa4c8231cd59473a1345d388176745fd05ba073b9599c0e159557acdc99685eb592e98da54baac595734ee13da8f743eeeeec15f05e5dafcb8c611c11233fecb29152b692015bf9b590a0b748e19fd1228dd11a9dcc1264dc00c61e1012ab57e77985299ca87f1bf65580b07ae23f68e759785e25ccbe8178b6a8c45176955caefc20a2d610d17a68b22575722ed19774ea68d855e928250fee66697042bf8d1c2788dd8633f3e6ffb046c948929db1475c0eba78970a607b245e7ab86af5b96dc70242665ef6b8b54ca8cbcf448b547fa89f34f032ab70af8c12e0866c0d29a27a28e5436e87d3b6850977a4ae49389207bfb2bc0f33b9784ad29e4bb474ecd97f18db9bcf9f2d08d688c9ce6dcc297816196e4b0db252ecd3350de79207316e111a7f3f643df11f2ac4365c2c642bd578cd61d271d329325db9136f383c5f3d20a9d66a2e3bd37530a898c0d0a3077f4f0eaced80f667eb86eab8f82fdb7907478ad65a43acca7abd89dc9f2a3f8101a3c73e0d3fda240a3aa5aef70ee06ecbfb3037d6ef1e19ebf8eb12fb3e97b95915b42a5f506bdc14e3b3c173bcf73ea1901313a6b3e8218a575f45c3a38b32583c62821b2030a2c72ec52bbe01d0790f1861d87f4991fd509607858f0b6c2e0b1b8f70c9361b1c23e14405a741ec30f72b17c726d294ca0ce8b9d132a3660b03cab2f10c7a23fe21a0ea39c743079e15b88d6ce79b8216950bdfb36da02b03530a3ae121494cfc9a3397940497bf962ce5cf1c28562cfcd91f614c8741b3ca4cf5a1733284358b610fd3634f73c67e09e152bccae689db3442d8c5b1532e58edac7cf46bb3868dc5e83d520cdf256e83e324c6b44329b70c4c4855c6597b6a10d5f4aec958b2e64112bd5c605ca2bd7ac86d8dd560aed15ffc640ede5bfeedd9ee4b61ac010ced17c33657fc31333539c2dfb59461af09e7049228113b5c9feea5a6e9959c18c51b19c90995afb9c76f2c0c820964cd7989c993a73925818a656c6a18dcd1a1e3782b2eae06dd5a41250ec2d1c203626ab9920c1673339eff04b1eb0cab85ef5909f571f9b83cdf21697c9f5cfa1c76e7bc4e1f395125c16e8322206d46643f6f41746c201a1a29c58c712b5e02aee73cef09d6329b7ebc7f355772e44da7c4", + "node_privkey": "4444444444444444444444444444444444444444444444444444444444444444", + "next_blinding": "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a" + }, + { + "onion": "0002ef43c4dfe9aee14248c445406ac7980474ce106c504d9025f57963739130adfd39eb24e9990cee396bc9c391962e42a62da9df4f34415db7020250442aba0c7daef9a2ffe21863db591ef4cd906cba336d562837320ea49893ce7c5a9a2d530a678f602256aa1ed9d4d82b4db92e21853631268565cdb67ae52e8583096674f1f17d167adbb3aabab7c2d84fbf94f5d827239f4c2b9d2c3cfe68fe5641f25e380af84245d986e06b2bfa0c666182acae5d86d535d985711e920ced57eb9087ebfbf219dd2ce0d1d4dfa4973bccfd2b751055d062d7548a1412f9fada657640a2b9c03d594401fbf210f428ff0394e27c9d9cf4b2e37c5a5767b3f13586a78a5e045d3726e64d385e67884c67fd056a7c0035d69b1d65bdd03fe43f465a11077c1aa9155eeeaa2e2649d6e5af53da9def458c776e523905d0ec7c0d3469a33cac03f822acf36efb0fdf496c209ff6381128224a30be86f3c3e2619e77a9b2650766f150d696e479671df69cf562f906cc746550fa0f650b9143c3ad75b9c059f14d09d3af03ebd9a7db5e616444480f76abe71e3c689d11c93210b1e077d891a669714535a979e0878c79276124d35fdb13ecf0f357503b0435d5b9f0e82d0df6326a530cffce25308f57f0d28c085616828480120bff1b25ac3331a0dec05689908431476e95ac027152ed5f1922204b2bf8f6e0653baeca78f7f53209dcdd1d27f59fbf275257ecea8e4706e8ed1400fb7e158aa5b763f28e39023eae7a45f8642ee6b8c91b624e11e4426e335b3be1435add21e6848f341548fe6097833304aaf34f1cffbf934010820e017bead53acb27051f899ed1f3d035b1f678068e65929a7c1a91e2b249258c0d0814e5bb0359a78922c8be1adf9b33c362101000b2f751fdc948f268e991d5a57ccbe3c85b7c5f197ef38da470494f52ad5d2504f3b5d951ec04b5c7c68a56088324384559aade7936a2e1320bd9c26f43be23d4dfb90d3b2df77ce1bb5afa205cdf33837cde4c329583c8ef639826847572374e17edd8f17d770ebca3d025988cb09c1bd705f3583f870ffd2460f7bacadf858c575c62dbff23e62c1b331b6b1f94e1c61dd901830fd84ca87fad7e358b432bec14a23cfe7edc9338845e466a773b2f009483b3b08bc06a910826d46d729f123c29a445a882398bf5c3a3b504e75be330218640b76d4e2ae940d3c47cd96c6444199fd565193ec46f337f1aff9e40cf5321e9a47f3af1694d0c93dfb8323670cd01a699d5c09b91aab9abbe1364ae42171c67e0333dfc651731a6846109c298e54e61188e27327bfbfbd11b4cd03a0b1c2dff70e129344b8710e74d95b6dec2d24e011c041c2a0d5cedf7783f7dba14b4fac8d9031b07a24a8423e12b3db4c3c4f8b42ad78f7fb38333fe27d0c1eab4dc14dc7541c35026511a5c7e0fc0a2875bbb3c4e5a31354c26343922d961b48662c94f170cfbd995c386f1b225eadafec7bc6ef45aebe6f6a095c3db19295dc7ef0ea0d2e0feb64ab404cca4293c08d62f19160bba880fa020795ea3f2f2d8124fb184d53437e109db27ec3b67a6ec240c44e3340740c15afdc36458d450d49c1ac1b7a7994577d096ce91f990e75abd9b3229c6b2414ab30a2c520169b5ca8e93a5a0d96a5294eceb9d54497145dbc5da7835e3c9e4e5edc4e19e57ba46b7f4f524247c352b1231149ba7fe7784c154fd8b0f9179ecdf1e9fd5c2939ec1ab16df9cbe9359101ebce933d4f65d3f66f87afaecfe9c046b52f4878b6c430329df7bd879fba8864fcbd9b782bf545734699b9b5a66b466dcedc0c9368803b5b0f1232950cef398ad3e057a5db964bd3e5c8a5717b30b41601acc321cb23ebe07b8f0cb6ea58d13cedb20ddc9eccfa6847ca86ea9819736d8e2235bbcb661445ec2343bb8bbcb2e", + "node_privkey": "4545454545454545454545454545454545454545454545454545454545454545", + "next_blinding": "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589" + } + ] + } +} From 91a33c83e24de9a340e8a843c97c48f1161adbc9 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 30 Jun 2022 21:53:38 +0200 Subject: [PATCH 4/4] cmd: add blinded path helper commands Add a blinded-key option to the parse command so that it can be used to test parsing of an onion for hops in a blinded route. Also add a helper nextBlindedKey command. --- cmd/main.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 52046e7..eb53b8f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -85,6 +85,25 @@ func main() { Usage: "The associated data to include", Value: defaultAssocData, }, + cli.StringFlag{ + Name: "blinding_point", + Usage: "The blinding point to use " + + "when parsing this onion.", + }, + }, + }, + { + Name: "nextblindedpub", + Action: nextBlindedPub, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "priv", + Required: true, + }, + cli.StringFlag{ + Name: "pub", + Required: true, + }, }, }, } @@ -228,9 +247,21 @@ func parse(ctx *cli.Context) error { if len(sessionKeyBytes) != 32 { return fmt.Errorf("session key must be 32 bytes") } - sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes) + var blindingPoint *btcec.PublicKey + if bpStr := ctx.String("blinding_point"); bpStr != "" { + bpBytes, err := hex.DecodeString(bpStr) + if err != nil { + return err + } + + blindingPoint, err = btcec.ParsePubKey(bpBytes) + if err != nil { + return err + } + } + onion, err := hex.DecodeString(ctx.String("onion")) if err != nil { return err @@ -250,7 +281,7 @@ func parse(ctx *cli.Context) error { defer s.Stop() p, err := s.ProcessOnionPacket( - &packet, []byte(ctx.String("assocData")), 10, nil, + &packet, []byte(ctx.String("assocData")), 10, blindingPoint, ) if err != nil { return err @@ -265,3 +296,34 @@ func parse(ctx *cli.Context) error { fmt.Printf("%x\n", w.Bytes()) return nil } + +func nextBlindedPub(ctx *cli.Context) error { + privKeyByte, err := hex.DecodeString(ctx.String("priv")) + if err != nil { + return err + } + if len(privKeyByte) != 32 { + return fmt.Errorf("private key must be 32 bytes") + } + privKey, _ := btcec.PrivKeyFromBytes(privKeyByte) + + pubKeyBytes, err := hex.DecodeString(ctx.String("pub")) + if err != nil { + return err + } + + pubKey, err := btcec.ParsePubKey(pubKeyBytes) + if err != nil { + return err + } + + nextBlindedKey, err := sphinx.NextEphemeral( + &sphinx.PrivKeyECDH{PrivKey: privKey}, pubKey, + ) + if err != nil { + return err + } + + fmt.Printf("%x\n", nextBlindedKey.SerializeCompressed()) + return nil +}