Merge pull request #4048 from wpaulino/tor-hashed-password

server+tor: add support for Tor HASHEDPASSWORD authentication method
This commit is contained in:
Wilmer Paulino 2020-03-10 11:12:41 -07:00 committed by GitHub
commit e17ad8bc84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 147 additions and 84 deletions

@ -221,6 +221,7 @@ type torConfig struct {
StreamIsolation bool `long:"streamisolation" description:"Enable Tor stream isolation by randomizing user credentials for each connection."` StreamIsolation bool `long:"streamisolation" description:"Enable Tor stream isolation by randomizing user credentials for each connection."`
Control string `long:"control" description:"The host:port that Tor is listening on for Tor control connections"` Control string `long:"control" description:"The host:port that Tor is listening on for Tor control connections"`
TargetIPAddress string `long:"targetipaddress" description:"IP address that Tor should use as the target of the hidden service"` TargetIPAddress string `long:"targetipaddress" description:"IP address that Tor should use as the target of the hidden service"`
Password string `long:"password" description:"The password used to arrive at the HashedControlPassword for the control port. If provided, the HASHEDPASSWORD authentication method will be used instead of the SAFECOOKIE one."`
V2 bool `long:"v2" description:"Automatically set up a v2 onion service to listen for inbound connections"` V2 bool `long:"v2" description:"Automatically set up a v2 onion service to listen for inbound connections"`
V3 bool `long:"v3" description:"Automatically set up a v3 onion service to listen for inbound connections"` V3 bool `long:"v3" description:"Automatically set up a v3 onion service to listen for inbound connections"`
PrivateKeyPath string `long:"privatekeypath" description:"The path to the private key of the onion service being created"` PrivateKeyPath string `long:"privatekeypath" description:"The path to the private key of the onion service being created"`

@ -2,7 +2,8 @@
1. [Overview](#overview) 1. [Overview](#overview)
2. [Getting Started](#getting-started) 2. [Getting Started](#getting-started)
3. [Tor Stream Isolation](#tor-stream-isolation) 3. [Tor Stream Isolation](#tor-stream-isolation)
4. [Listening for Inbound Connections](#listening-for-inbound-connections) 4. [Authentication](#authentication)
5. [Listening for Inbound Connections](#listening-for-inbound-connections)
## Overview ## Overview
@ -78,6 +79,8 @@ Tor:
--tor.dns= The DNS server as host:port that Tor will use for SRV queries - NOTE must have TCP resolution enabled (default: soa.nodes.lightning.directory:53) --tor.dns= The DNS server as host:port that Tor will use for SRV queries - NOTE must have TCP resolution enabled (default: soa.nodes.lightning.directory:53)
--tor.streamisolation Enable Tor stream isolation by randomizing user credentials for each connection. --tor.streamisolation Enable Tor stream isolation by randomizing user credentials for each connection.
--tor.control= The host:port that Tor is listening on for Tor control connections (default: localhost:9051) --tor.control= The host:port that Tor is listening on for Tor control connections (default: localhost:9051)
--tor.targetipaddress= IP address that Tor should use as the target of the hidden service
--tor.password= The password used to arrive at the HashedControlPassword for the control port. If provided, the HASHEDPASSWORD authentication method will be used instead of the SAFECOOKIE one.
--tor.v2 Automatically set up a v2 onion service to listen for inbound connections --tor.v2 Automatically set up a v2 onion service to listen for inbound connections
--tor.v3 Automatically set up a v3 onion service to listen for inbound connections --tor.v3 Automatically set up a v3 onion service to listen for inbound connections
--tor.privatekeypath= The path to the private key of the onion service being created --tor.privatekeypath= The path to the private key of the onion service being created
@ -133,6 +136,26 @@ specification of an additional argument:
⛰ ./lnd --tor.active --tor.streamisolation ⛰ ./lnd --tor.active --tor.streamisolation
``` ```
## Authentication
In order for `lnd` to communicate with the Tor daemon securely, it must first
establish an authenticated connection. `lnd` supports the following Tor control
authentication methods (arguably, from most to least secure):
* `SAFECOOKIE`: This authentication method relies on a cookie created and
stored by the Tor daemon and is the default assuming the Tor daemon supports
it by specifying `CookieAuthentication 1` in its configuration file.
* `HASHEDPASSWORD`: This authentication method is stateless as it relies on a
password hash scheme and may be useful if the Tor daemon is operating under a
separate host from the `lnd` node. The password hash can be obtained through
the Tor daemon with `tor --hash-password PASSWORD`, which should then be
specified in Tor's configuration file with `HashedControlPassword
PASSWORD_HASH`. Finally, to use it within `lnd`, the `--tor.password` flag
should be provided with the corresponding password.
* `NULL`: To bypass any authentication at all, this scheme can be used instead.
It doesn't require any additional flags to `lnd` or configuration options to
the Tor daemon.
## Listening for Inbound Connections ## Listening for Inbound Connections
In order to listen for inbound connections through Tor, an onion service must be In order to listen for inbound connections through Tor, an onion service must be

@ -594,7 +594,10 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
// automatically create an onion service, we'll initiate our Tor // automatically create an onion service, we'll initiate our Tor
// controller and establish a connection to the Tor server. // controller and establish a connection to the Tor server.
if cfg.Tor.Active && (cfg.Tor.V2 || cfg.Tor.V3) { if cfg.Tor.Active && (cfg.Tor.V2 || cfg.Tor.V3) {
s.torController = tor.NewController(cfg.Tor.Control, cfg.Tor.TargetIPAddress) s.torController = tor.NewController(
cfg.Tor.Control, cfg.Tor.TargetIPAddress,
cfg.Tor.Password,
)
} }
chanGraph := chanDB.ChannelGraph() chanGraph := chanDB.ChannelGraph()

@ -8,8 +8,8 @@ Tor daemon. So far, supported functions include:
* Routing DNS queries over Tor (A, AAAA, SRV). * Routing DNS queries over Tor (A, AAAA, SRV).
* Limited Tor Control functionality (synchronous messages only). So far, this * Limited Tor Control functionality (synchronous messages only). So far, this
includes: includes:
* Support for SAFECOOKIE authentication only as a sane default. * Support for SAFECOOKIE, HASHEDPASSWORD, and NULL authentication methods.
* Creating v2 onion services. * Creating v2 and v3 onion services.
In the future, the Tor Control functionality will be extended to support v3 In the future, the Tor Control functionality will be extended to support v3
onion services, asynchronous messages, etc. onion services, asynchronous messages, etc.

@ -36,6 +36,16 @@ const (
// must be running on. This is needed in order to create v3 onion // must be running on. This is needed in order to create v3 onion
// services through Tor's control port. // services through Tor's control port.
MinTorVersion = "0.3.3.6" MinTorVersion = "0.3.3.6"
// authSafeCookie is the name of the SAFECOOKIE authentication method.
authSafeCookie = "SAFECOOKIE"
// authHashedPassword is the name of the HASHEDPASSWORD authentication
// method.
authHashedPassword = "HASHEDPASSWORD"
// authNull is the name of the NULL authentication method.
authNull = "NULL"
) )
var ( var (
@ -79,19 +89,30 @@ type Controller struct {
// controller connections on. // controller connections on.
controlAddr string controlAddr string
// password, if non-empty, signals that the controller should attempt to
// authenticate itself with the backing Tor daemon through the
// HASHEDPASSWORD authentication method with this value.
password string
// version is the current version of the Tor server. // version is the current version of the Tor server.
version string version string
// The IP address which we tell the Tor server to use to connect to the LND node. // targetIPAddress is the IP address which we tell the Tor server to use
// This is required when the Tor server runs on another host, otherwise the service // to connect to the LND node. This is required when the Tor server
// will not be reachable. // runs on another host, otherwise the service will not be reachable.
targetIPAddress string targetIPAddress string
} }
// NewController returns a new Tor controller that will be able to interact with // NewController returns a new Tor controller that will be able to interact with
// a Tor server. // a Tor server.
func NewController(controlAddr string, targetIPAddress string) *Controller { func NewController(controlAddr string, targetIPAddress string,
return &Controller{controlAddr: controlAddr, targetIPAddress: targetIPAddress} password string) *Controller {
return &Controller{
controlAddr: controlAddr,
targetIPAddress: targetIPAddress,
password: password,
}
} }
// Start establishes and authenticates the connection between the controller and // Start establishes and authenticates the connection between the controller and
@ -168,26 +189,74 @@ func parseTorReply(reply string) map[string]string {
} }
// authenticate authenticates the connection between the controller and the // authenticate authenticates the connection between the controller and the
// Tor server using the SAFECOOKIE or NULL authentication method. // Tor server using either of the following supported authentication methods
// depending on its configuration: SAFECOOKIE, HASHEDPASSWORD, and NULL.
func (c *Controller) authenticate() error { func (c *Controller) authenticate() error {
protocolInfo, err := c.protocolInfo()
if err != nil {
return err
}
// With the version retrieved, we'll cache it now in case it needs to be
// used later on.
c.version = protocolInfo.version()
switch {
// If a password was provided, then we should attempt to use the
// HASHEDPASSWORD authentication method.
case c.password != "":
if !protocolInfo.supportsAuthMethod(authHashedPassword) {
return fmt.Errorf("%v authentication method not "+
"supported", authHashedPassword)
}
return c.authenticateViaHashedPassword()
// Otherwise, attempt to authentication via the SAFECOOKIE method as it
// provides the most security.
case protocolInfo.supportsAuthMethod(authSafeCookie):
return c.authenticateViaSafeCookie(protocolInfo)
// Fallback to the NULL method if any others aren't supported.
case protocolInfo.supportsAuthMethod(authNull):
return c.authenticateViaNull()
// No supported authentication methods, fail.
default:
return errors.New("the Tor server must be configured with " +
"NULL, SAFECOOKIE, or HASHEDPASSWORD authentication")
}
}
// authenticateViaNull authenticates the controller with the Tor server using
// the NULL authentication method.
func (c *Controller) authenticateViaNull() error {
_, _, err := c.sendCommand("AUTHENTICATE")
return err
}
// authenticateViaHashedPassword authenticates the controller with the Tor
// server using the HASHEDPASSWORD authentication method.
func (c *Controller) authenticateViaHashedPassword() error {
cmd := fmt.Sprintf("AUTHENTICATE \"%s\"", c.password)
_, _, err := c.sendCommand(cmd)
return err
}
// authenticateViaSafeCookie authenticates the controller with the Tor server
// using the SAFECOOKIE authentication method.
func (c *Controller) authenticateViaSafeCookie(info protocolInfo) error {
// Before proceeding to authenticate the connection, we'll retrieve // Before proceeding to authenticate the connection, we'll retrieve
// the authentication cookie of the Tor server. This will be used // the authentication cookie of the Tor server. This will be used
// throughout the authentication routine. We do this before as once the // throughout the authentication routine. We do this before as once the
// authentication routine has begun, it is not possible to retrieve it // authentication routine has begun, it is not possible to retrieve it
// mid-way. // mid-way.
cookie, err := c.getAuthCookie() cookie, err := c.getAuthCookie(info)
if err != nil { if err != nil {
return fmt.Errorf("unable to retrieve authentication cookie: "+ return fmt.Errorf("unable to retrieve authentication cookie: "+
"%v", err) "%v", err)
} }
// If cookie is empty and there's no error, we have a NULL
// authentication method that we should use instead.
if len(cookie) == 0 {
_, _, err := c.sendCommand("AUTHENTICATE")
return err
}
// Authenticating using the SAFECOOKIE authentication method is a two // Authenticating using the SAFECOOKIE authentication method is a two
// step process. We'll kick off the authentication routine by sending // step process. We'll kick off the authentication routine by sending
// the AUTHCHALLENGE command followed by a hex-encoded 32-byte nonce. // the AUTHCHALLENGE command followed by a hex-encoded 32-byte nonce.
@ -272,36 +341,15 @@ func (c *Controller) authenticate() error {
} }
// getAuthCookie retrieves the authentication cookie in bytes from the Tor // getAuthCookie retrieves the authentication cookie in bytes from the Tor
// server. Cookie authentication must be enabled for this to work. The boolean // server. Cookie authentication must be enabled for this to work.
func (c *Controller) getAuthCookie() ([]byte, error) { func (c *Controller) getAuthCookie(info protocolInfo) ([]byte, error) {
// Retrieve the authentication methods currently supported by the Tor // Retrieve the cookie file path from the PROTOCOLINFO reply.
// server. cookieFilePath, ok := info["COOKIEFILE"]
authMethods, cookieFilePath, version, err := c.ProtocolInfo() if !ok {
if err != nil { return nil, errors.New("COOKIEFILE not found in PROTOCOLINFO " +
return nil, err "reply")
}
// With the version retrieved, we'll cache it now in case it needs to be
// used later on.
c.version = version
// Ensure that the Tor server supports the SAFECOOKIE authentication
// method or the NULL method. If NULL, we don't need the cookie info
// below this loop, so we just return.
safeCookieSupport := false
for _, authMethod := range authMethods {
if authMethod == "SAFECOOKIE" {
safeCookieSupport = true
}
if authMethod == "NULL" {
return nil, nil
}
}
if !safeCookieSupport {
return nil, errors.New("the Tor server is currently not " +
"configured for cookie or null authentication")
} }
cookieFilePath = strings.Trim(cookieFilePath, "\"")
// Read the cookie from the file and ensure it has the correct length. // Read the cookie from the file and ensure it has the correct length.
cookie, err := ioutil.ReadFile(cookieFilePath) cookie, err := ioutil.ReadFile(cookieFilePath)
@ -360,48 +408,36 @@ func supportsV3(version string) error {
return nil return nil
} }
// ProtocolInfo returns the different authentication methods supported by the // protocolInfo is encompasses the details of a response to a PROTOCOLINFO
// Tor server and the version of the Tor server. // command.
func (c *Controller) ProtocolInfo() ([]string, string, string, error) { type protocolInfo map[string]string
// We'll start off by sending the "PROTOCOLINFO" command to the Tor
// server. We should receive a reply of the following format: // version returns the Tor version as reported by the server.
// func (i protocolInfo) version() string {
// METHODS=COOKIE,SAFECOOKIE version := i["Tor"]
// COOKIEFILE="/home/user/.tor/control_auth_cookie" return strings.Trim(version, "\"")
// VERSION Tor="0.3.2.10" }
//
// We're interested in retrieving all of these fields, so we'll parse // supportsAuthMethod determines whether the Tor server supports the given
// our reply to do so. // authentication method.
func (i protocolInfo) supportsAuthMethod(method string) bool {
methods, ok := i["METHODS"]
if !ok {
return false
}
return strings.Contains(methods, method)
}
// protocolInfo sends a "PROTOCOLINFO" command to the Tor server and returns its
// response.
func (c *Controller) protocolInfo() (protocolInfo, error) {
cmd := fmt.Sprintf("PROTOCOLINFO %d", ProtocolInfoVersion) cmd := fmt.Sprintf("PROTOCOLINFO %d", ProtocolInfoVersion)
_, reply, err := c.sendCommand(cmd) _, reply, err := c.sendCommand(cmd)
if err != nil { if err != nil {
return nil, "", "", err return nil, err
} }
info := parseTorReply(reply) return protocolInfo(parseTorReply(reply)), nil
methods, ok := info["METHODS"]
if !ok {
return nil, "", "", errors.New("auth methods not found in " +
"reply")
}
cookieFile, ok := info["COOKIEFILE"]
if !ok && !strings.Contains(methods, "NULL") {
return nil, "", "", errors.New("cookie file path not found " +
"in reply")
}
version, ok := info["Tor"]
if !ok {
return nil, "", "", errors.New("Tor version not found in reply")
}
// Finally, we'll clean up the results before returning them.
authMethods := strings.Split(methods, ",")
cookieFilePath := strings.Trim(cookieFile, "\"")
torVersion := strings.Trim(version, "\"")
return authMethods, cookieFilePath, torVersion, nil
} }
// OnionType denotes the type of the onion service. // OnionType denotes the type of the onion service.