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.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..a36349b --- /dev/null +++ b/blinding_test.go @@ -0,0 +1,332 @@ +package sphinx + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "io/ioutil" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/require" +) + +const ( + routeBlindingTestFileName = "testdata/route-blinding-test.json" + onionRouteBlindingTestFileName = "testdata/onion-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, + )) + } +} + +// 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"` + 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/cmd/main.go b/cmd/main.go index 5266555..eb53b8f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,157 +8,322 @@ 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, + }, + 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, + }, + }, + }, + } + + 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, - ) + fmt.Printf("%x\n", w.Bytes()) + return nil +} + +func parse(ctx *cli.Context) error { + sessionKeyBytes, err := hex.DecodeString(ctx.String("priv")) + if err != nil { + return err + } + + 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 { - log.Fatalf("Error creating message: %v", err) + return err } - w := bytes.NewBuffer([]byte{}) - err = msg.Encode(w) + blindingPoint, err = btcec.ParsePubKey(bpBytes) if err != nil { - log.Fatalf("Error serializing message: %v", err) + 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") - } + onion, err := hex.DecodeString(ctx.String("onion")) + if err != nil { + return err + } - hexBytes, _ := ioutil.ReadAll(os.Stdin) - binMsg, err := hex.DecodeString(strings.TrimSpace(string(hexBytes))) - if err != nil { - log.Fatalf("Error decoding message: %s", err) - } + var packet sphinx.OnionPacket + err = packet.Decode(bytes.NewBuffer(onion)) + if err != nil { + return err + } - privkey, _ := btcec.PrivKeyFromBytes(binKey) - privKeyECDH := &sphinx.PrivKeyECDH{PrivKey: privkey} - replayLog := sphinx.NewMemoryReplayLog() - s := sphinx.NewRouter( - privKeyECDH, &chaincfg.TestNet3Params, replayLog, - ) + s := sphinx.NewRouter( + &sphinx.PrivKeyECDH{PrivKey: sessionKey}, + &chaincfg.TestNet3Params, sphinx.NewMemoryReplayLog(), + ) + s.Start() + defer s.Stop() - replayLog.Start() - defer replayLog.Stop() + p, err := s.ProcessOnionPacket( + &packet, []byte(ctx.String("assocData")), 10, blindingPoint, + ) + if err != nil { + return err + } - var packet sphinx.OnionPacket - err = packet.Decode(bytes.NewBuffer(binMsg)) + w := bytes.NewBuffer([]byte{}) + err = p.NextPacket.Encode(w) - 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) - } + if err != nil { + log.Fatalf("Error serializing message: %v", err) + } + fmt.Printf("%x\n", w.Bytes()) + return nil +} - w := bytes.NewBuffer([]byte{}) - err = p.NextPacket.Encode(w) +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) - if err != nil { - log.Fatalf("Error serializing message: %v", err) - } - fmt.Printf("%x\n", w.Bytes()) + 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 } diff --git a/crypto.go b/crypto.go index 939f9ec..bfc9442 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. // @@ -209,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/go.mod b/go.mod index d274362..6118a0b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ 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 fe83050..40b396b 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,7 +21,10 @@ 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.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= @@ -53,6 +57,19 @@ 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/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= 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= @@ -85,9 +102,14 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.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/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" + } + ] + } +} 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