From 46f4da0b6b768e7d361acd465c665d8357533495 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Fri, 6 Mar 2020 16:01:47 -0800 Subject: [PATCH 1/2] tor: move AddOnion to its own file --- tor/add_onion.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++ tor/controller.go | 142 ------------------------------------------- 2 files changed, 149 insertions(+), 142 deletions(-) create mode 100644 tor/add_onion.go diff --git a/tor/add_onion.go b/tor/add_onion.go new file mode 100644 index 00000000..8825144e --- /dev/null +++ b/tor/add_onion.go @@ -0,0 +1,149 @@ +package tor + +import ( + "errors" + "fmt" + "io/ioutil" + "os" +) + +// OnionType denotes the type of the onion service. +type OnionType int + +const ( + // V2 denotes that the onion service is V2. + V2 OnionType = iota + + // V3 denotes that the onion service is V3. + V3 +) + +// AddOnionConfig houses all of the required parameters in order to successfully +// create a new onion service or restore an existing one. +type AddOnionConfig struct { + // Type denotes the type of the onion service that should be created. + Type OnionType + + // VirtualPort is the externally reachable port of the onion address. + VirtualPort int + + // TargetPorts is the set of ports that the service will be listening on + // locally. The Tor server will use choose a random port from this set + // to forward the traffic from the virtual port. + // + // NOTE: If nil/empty, the virtual port will be used as the only target + // port. + TargetPorts []int + + // PrivateKeyPath is the full path to where the onion service's private + // key is stored. This can be used to restore an existing onion service. + PrivateKeyPath string +} + +// AddOnion creates an onion service and returns its onion address. Once +// created, the new onion service will remain active until the connection +// between the controller and the Tor server is closed. +func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) { + // Before sending the request to create an onion service to the Tor + // server, we'll make sure that it supports V3 onion services if that + // was the type requested. + if cfg.Type == V3 { + if err := supportsV3(c.version); err != nil { + return nil, err + } + } + + // We'll start off by checking if the file containing the private key + // exists. If it does not, then we should request the server to create + // a new onion service and return its private key. Otherwise, we'll + // request the server to recreate the onion server from our private key. + var keyParam string + if _, err := os.Stat(cfg.PrivateKeyPath); os.IsNotExist(err) { + switch cfg.Type { + case V2: + keyParam = "NEW:RSA1024" + case V3: + keyParam = "NEW:ED25519-V3" + } + } else { + privateKey, err := ioutil.ReadFile(cfg.PrivateKeyPath) + if err != nil { + return nil, err + } + keyParam = string(privateKey) + } + + // Now, we'll create a mapping from the virtual port to each target + // port. If no target ports were specified, we'll use the virtual port + // to provide a one-to-one mapping. + var portParam string + + // Helper function which appends the correct Port param depending on + // whether the user chose to use a custom target IP address or not. + pushPortParam := func(targetPort int) { + if c.targetIPAddress == "" { + portParam += fmt.Sprintf("Port=%d,%d ", cfg.VirtualPort, + targetPort) + } else { + portParam += fmt.Sprintf("Port=%d,%s:%d ", cfg.VirtualPort, + c.targetIPAddress, targetPort) + } + } + + if len(cfg.TargetPorts) == 0 { + pushPortParam(cfg.VirtualPort) + } else { + for _, targetPort := range cfg.TargetPorts { + pushPortParam(targetPort) + } + } + + // Send the command to create the onion service to the Tor server and + // await its response. + cmd := fmt.Sprintf("ADD_ONION %s %s", keyParam, portParam) + _, reply, err := c.sendCommand(cmd) + if err != nil { + return nil, err + } + + // If successful, the reply from the server should be of the following + // format, depending on whether a private key has been requested: + // + // C: ADD_ONION RSA1024:[Blob Redacted] Port=80,8080 + // S: 250-ServiceID=testonion1234567 + // S: 250 OK + // + // C: ADD_ONION NEW:RSA1024 Port=80,8080 + // S: 250-ServiceID=testonion1234567 + // S: 250-PrivateKey=RSA1024:[Blob Redacted] + // S: 250 OK + // + // We're interested in retrieving the service ID, which is the public + // name of the service, and the private key if requested. + replyParams := parseTorReply(reply) + serviceID, ok := replyParams["ServiceID"] + if !ok { + return nil, errors.New("service id not found in reply") + } + + // If a new onion service was created, we'll write its private key to + // disk under strict permissions in the event that it needs to be + // recreated later on. + if privateKey, ok := replyParams["PrivateKey"]; ok { + err := ioutil.WriteFile( + cfg.PrivateKeyPath, []byte(privateKey), 0600, + ) + if err != nil { + return nil, fmt.Errorf("unable to write private key "+ + "to file: %v", err) + } + } + + // Finally, we'll return the onion address composed of the service ID, + // along with the onion suffix, and the port this onion service can be + // reached at externally. + return &OnionAddr{ + OnionService: serviceID + ".onion", + Port: cfg.VirtualPort, + }, nil +} diff --git a/tor/controller.go b/tor/controller.go index 149e766b..48d71c65 100644 --- a/tor/controller.go +++ b/tor/controller.go @@ -10,7 +10,6 @@ import ( "fmt" "io/ioutil" "net/textproto" - "os" "strconv" "strings" "sync/atomic" @@ -403,144 +402,3 @@ func (c *Controller) ProtocolInfo() ([]string, string, string, error) { return authMethods, cookieFilePath, torVersion, nil } - -// OnionType denotes the type of the onion service. -type OnionType int - -const ( - // V2 denotes that the onion service is V2. - V2 OnionType = iota - - // V3 denotes that the onion service is V3. - V3 -) - -// AddOnionConfig houses all of the required parameters in order to successfully -// create a new onion service or restore an existing one. -type AddOnionConfig struct { - // Type denotes the type of the onion service that should be created. - Type OnionType - - // VirtualPort is the externally reachable port of the onion address. - VirtualPort int - - // TargetPorts is the set of ports that the service will be listening on - // locally. The Tor server will use choose a random port from this set - // to forward the traffic from the virtual port. - // - // NOTE: If nil/empty, the virtual port will be used as the only target - // port. - TargetPorts []int - - // PrivateKeyPath is the full path to where the onion service's private - // key is stored. This can be used to restore an existing onion service. - PrivateKeyPath string -} - -// AddOnion creates an onion service and returns its onion address. Once -// created, the new onion service will remain active until the connection -// between the controller and the Tor server is closed. -func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) { - // Before sending the request to create an onion service to the Tor - // server, we'll make sure that it supports V3 onion services if that - // was the type requested. - if cfg.Type == V3 { - if err := supportsV3(c.version); err != nil { - return nil, err - } - } - - // We'll start off by checking if the file containing the private key - // exists. If it does not, then we should request the server to create - // a new onion service and return its private key. Otherwise, we'll - // request the server to recreate the onion server from our private key. - var keyParam string - if _, err := os.Stat(cfg.PrivateKeyPath); os.IsNotExist(err) { - switch cfg.Type { - case V2: - keyParam = "NEW:RSA1024" - case V3: - keyParam = "NEW:ED25519-V3" - } - } else { - privateKey, err := ioutil.ReadFile(cfg.PrivateKeyPath) - if err != nil { - return nil, err - } - keyParam = string(privateKey) - } - - // Now, we'll create a mapping from the virtual port to each target - // port. If no target ports were specified, we'll use the virtual port - // to provide a one-to-one mapping. - var portParam string - - // Helper function which appends the correct Port param depending on - // whether the user chose to use a custom target IP address or not. - pushPortParam := func(targetPort int) { - if c.targetIPAddress == "" { - portParam += fmt.Sprintf("Port=%d,%d ", cfg.VirtualPort, - targetPort) - } else { - portParam += fmt.Sprintf("Port=%d,%s:%d ", cfg.VirtualPort, - c.targetIPAddress, targetPort) - } - } - - if len(cfg.TargetPorts) == 0 { - pushPortParam(cfg.VirtualPort) - } else { - for _, targetPort := range cfg.TargetPorts { - pushPortParam(targetPort) - } - } - - // Send the command to create the onion service to the Tor server and - // await its response. - cmd := fmt.Sprintf("ADD_ONION %s %s", keyParam, portParam) - _, reply, err := c.sendCommand(cmd) - if err != nil { - return nil, err - } - - // If successful, the reply from the server should be of the following - // format, depending on whether a private key has been requested: - // - // C: ADD_ONION RSA1024:[Blob Redacted] Port=80,8080 - // S: 250-ServiceID=testonion1234567 - // S: 250 OK - // - // C: ADD_ONION NEW:RSA1024 Port=80,8080 - // S: 250-ServiceID=testonion1234567 - // S: 250-PrivateKey=RSA1024:[Blob Redacted] - // S: 250 OK - // - // We're interested in retrieving the service ID, which is the public - // name of the service, and the private key if requested. - replyParams := parseTorReply(reply) - serviceID, ok := replyParams["ServiceID"] - if !ok { - return nil, errors.New("service id not found in reply") - } - - // If a new onion service was created, we'll write its private key to - // disk under strict permissions in the event that it needs to be - // recreated later on. - if privateKey, ok := replyParams["PrivateKey"]; ok { - err := ioutil.WriteFile( - cfg.PrivateKeyPath, []byte(privateKey), 0600, - ) - if err != nil { - return nil, fmt.Errorf("unable to write private key "+ - "to file: %v", err) - } - } - - // Finally, we'll return the onion address composed of the service ID, - // along with the onion suffix, and the port this onion service can be - // reached at externally. - return &OnionAddr{ - OnionService: serviceID + ".onion", - Port: cfg.VirtualPort, - }, nil -} From 2dfd6a7a3e28b3269b8c2ecb5cb387e114242724 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Fri, 6 Mar 2020 16:26:51 -0800 Subject: [PATCH 2/2] tor+server: add OnionStore to AddOnionConfig with file-based impl In this commit, we modify the AddOnionConfig struct to include an abstract OnionStore, which will be responsible for storing all relevant information of an onion service. We also add a file-based implementation of the interface to maintain the same behavior of storing an onion service's private key in a file. --- server.go | 6 +-- tor/add_onion.go | 114 +++++++++++++++++++++++++++++++++--------- tor/add_onion_test.go | 51 +++++++++++++++++++ 3 files changed, 145 insertions(+), 26 deletions(-) create mode 100644 tor/add_onion_test.go diff --git a/server.go b/server.go index bde65671..5c6fce55 100644 --- a/server.go +++ b/server.go @@ -1965,9 +1965,9 @@ func (s *server) initTorController() error { // create our onion service. The service's private key will be saved to // disk in order to regain access to this service when restarting `lnd`. onionCfg := tor.AddOnionConfig{ - VirtualPort: defaultPeerPort, - TargetPorts: listenPorts, - PrivateKeyPath: cfg.Tor.PrivateKeyPath, + VirtualPort: defaultPeerPort, + TargetPorts: listenPorts, + Store: tor.NewOnionFile(cfg.Tor.PrivateKeyPath, 0600), } switch { diff --git a/tor/add_onion.go b/tor/add_onion.go index 8825144e..aca433ad 100644 --- a/tor/add_onion.go +++ b/tor/add_onion.go @@ -7,6 +7,12 @@ import ( "os" ) +var ( + // ErrNoPrivateKey is an error returned by the OnionStore.PrivateKey + // method when a private key hasn't yet been stored. + ErrNoPrivateKey = errors.New("private key not found") +) + // OnionType denotes the type of the onion service. type OnionType int @@ -18,6 +24,60 @@ const ( V3 ) +// OnionStore is a store containing information about a particular onion +// service. +type OnionStore interface { + // StorePrivateKey stores the private key according to the + // implementation of the OnionStore interface. + StorePrivateKey(OnionType, []byte) error + + // PrivateKey retrieves a stored private key. If it is not found, then + // ErrNoPrivateKey should be returned. + PrivateKey(OnionType) ([]byte, error) + + // DeletePrivateKey securely removes the private key from the store. + DeletePrivateKey(OnionType) error +} + +// OnionFile is a file-based implementation of the OnionStore interface that +// stores an onion service's private key. +type OnionFile struct { + privateKeyPath string + privateKeyPerm os.FileMode +} + +// A compile-time constraint to ensure OnionFile satisfies the OnionStore +// interface. +var _ OnionStore = (*OnionFile)(nil) + +// NewOnionFile creates a file-based implementation of the OnionStore interface +// to store an onion service's private key. +func NewOnionFile(privateKeyPath string, privateKeyPerm os.FileMode) *OnionFile { + return &OnionFile{ + privateKeyPath: privateKeyPath, + privateKeyPerm: privateKeyPerm, + } +} + +// StorePrivateKey stores the private key at its expected path. +func (f *OnionFile) StorePrivateKey(_ OnionType, privateKey []byte) error { + return ioutil.WriteFile(f.privateKeyPath, privateKey, f.privateKeyPerm) +} + +// PrivateKey retrieves the private key from its expected path. If the file does +// not exist, then ErrNoPrivateKey is returned. +func (f *OnionFile) PrivateKey(_ OnionType) ([]byte, error) { + if _, err := os.Stat(f.privateKeyPath); os.IsNotExist(err) { + return nil, ErrNoPrivateKey + } + return ioutil.ReadFile(f.privateKeyPath) +} + +// DeletePrivateKey removes the file containing the private key. +func (f *OnionFile) DeletePrivateKey(_ OnionType) error { + return os.Remove(f.privateKeyPath) +} + // AddOnionConfig houses all of the required parameters in order to successfully // create a new onion service or restore an existing one. type AddOnionConfig struct { @@ -35,9 +95,12 @@ type AddOnionConfig struct { // port. TargetPorts []int - // PrivateKeyPath is the full path to where the onion service's private - // key is stored. This can be used to restore an existing onion service. - PrivateKeyPath string + // Store is responsible for storing all onion service related + // information. + // + // NOTE: If not specified, then nothing will be stored, making onion + // services unrecoverable after shutdown. + Store OnionStore } // AddOnion creates an onion service and returns its onion address. Once @@ -53,24 +116,31 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) { } } - // We'll start off by checking if the file containing the private key - // exists. If it does not, then we should request the server to create - // a new onion service and return its private key. Otherwise, we'll + // We'll start off by checking if the store contains an existing private + // key. If it does not, then we should request the server to create a + // new onion service and return its private key. Otherwise, we'll // request the server to recreate the onion server from our private key. var keyParam string - if _, err := os.Stat(cfg.PrivateKeyPath); os.IsNotExist(err) { - switch cfg.Type { - case V2: - keyParam = "NEW:RSA1024" - case V3: - keyParam = "NEW:ED25519-V3" - } - } else { - privateKey, err := ioutil.ReadFile(cfg.PrivateKeyPath) - if err != nil { + switch cfg.Type { + case V2: + keyParam = "NEW:RSA1024" + case V3: + keyParam = "NEW:ED25519-V3" + } + + if cfg.Store != nil { + privateKey, err := cfg.Store.PrivateKey(cfg.Type) + switch err { + // Proceed to request a new onion service. + case ErrNoPrivateKey: + + // Recover the onion service with the private key found. + case nil: + keyParam = string(privateKey) + + default: return nil, err } - keyParam = string(privateKey) } // Now, we'll create a mapping from the virtual port to each target @@ -126,13 +196,11 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) { return nil, errors.New("service id not found in reply") } - // If a new onion service was created, we'll write its private key to - // disk under strict permissions in the event that it needs to be + // If a new onion service was created and an onion store was provided, + // we'll store its private key to disk in the event that it needs to be // recreated later on. - if privateKey, ok := replyParams["PrivateKey"]; ok { - err := ioutil.WriteFile( - cfg.PrivateKeyPath, []byte(privateKey), 0600, - ) + if privateKey, ok := replyParams["PrivateKey"]; cfg.Store != nil && ok { + err := cfg.Store.StorePrivateKey(cfg.Type, []byte(privateKey)) if err != nil { return nil, fmt.Errorf("unable to write private key "+ "to file: %v", err) diff --git a/tor/add_onion_test.go b/tor/add_onion_test.go new file mode 100644 index 00000000..9cd255d3 --- /dev/null +++ b/tor/add_onion_test.go @@ -0,0 +1,51 @@ +package tor + +import ( + "bytes" + "io/ioutil" + "path/filepath" + "testing" +) + +// TestOnionFile tests that the OnionFile implementation of the OnionStore +// interface behaves as expected. +func TestOnionFile(t *testing.T) { + t.Parallel() + + tempDir, err := ioutil.TempDir("", "onion_store") + if err != nil { + t.Fatalf("unable to create temp dir: %v", err) + } + + privateKey := []byte("hide_me_plz") + privateKeyPath := filepath.Join(tempDir, "secret") + + // Create a new file-based onion store. A private key should not exist + // yet. + onionFile := NewOnionFile(privateKeyPath, 0600) + if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey { + t.Fatalf("expected ErrNoPrivateKey, got \"%v\"", err) + } + + // Store the private key and ensure what's stored matches. + if err := onionFile.StorePrivateKey(V2, privateKey); err != nil { + t.Fatalf("unable to store private key: %v", err) + } + storePrivateKey, err := onionFile.PrivateKey(V2) + if err != nil { + t.Fatalf("unable to retrieve private key: %v", err) + } + if !bytes.Equal(storePrivateKey, privateKey) { + t.Fatalf("expected private key \"%v\", got \"%v\"", + string(privateKey), string(storePrivateKey)) + } + + // Finally, delete the private key. We should no longer be able to + // retrieve it. + if err := onionFile.DeletePrivateKey(V2); err != nil { + t.Fatalf("unable to delete private key: %v", err) + } + if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey { + t.Fatal("found deleted private key") + } +}