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."`
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"`
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"`
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"`

@ -2,7 +2,8 @@
1. [Overview](#overview)
2. [Getting Started](#getting-started)
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
@ -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.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.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.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
@ -133,6 +136,26 @@ specification of an additional argument:
⛰ ./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
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
// controller and establish a connection to the Tor server.
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()

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

@ -36,6 +36,16 @@ const (
// must be running on. This is needed in order to create v3 onion
// services through Tor's control port.
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 (
@ -79,19 +89,30 @@ type Controller struct {
// controller connections on.
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 string
// The IP address which we tell the Tor server to use to connect to the LND node.
// This is required when the Tor server runs on another host, otherwise the service
// will not be reachable.
// targetIPAddress is the IP address which we tell the Tor server to use
// to connect to the LND node. This is required when the Tor server
// runs on another host, otherwise the service will not be reachable.
targetIPAddress string
}
// NewController returns a new Tor controller that will be able to interact with
// a Tor server.
func NewController(controlAddr string, targetIPAddress string) *Controller {
return &Controller{controlAddr: controlAddr, targetIPAddress: targetIPAddress}
func NewController(controlAddr string, targetIPAddress string,
password string) *Controller {
return &Controller{
controlAddr: controlAddr,
targetIPAddress: targetIPAddress,
password: password,
}
}
// 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
// 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 {
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
// the authentication cookie of the Tor server. This will be used
// throughout the authentication routine. We do this before as once the
// authentication routine has begun, it is not possible to retrieve it
// mid-way.
cookie, err := c.getAuthCookie()
cookie, err := c.getAuthCookie(info)
if err != nil {
return fmt.Errorf("unable to retrieve authentication cookie: "+
"%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
// step process. We'll kick off the authentication routine by sending
// 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
// server. Cookie authentication must be enabled for this to work. The boolean
func (c *Controller) getAuthCookie() ([]byte, error) {
// Retrieve the authentication methods currently supported by the Tor
// server.
authMethods, cookieFilePath, version, err := c.ProtocolInfo()
if err != nil {
return nil, err
}
// 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")
// server. Cookie authentication must be enabled for this to work.
func (c *Controller) getAuthCookie(info protocolInfo) ([]byte, error) {
// Retrieve the cookie file path from the PROTOCOLINFO reply.
cookieFilePath, ok := info["COOKIEFILE"]
if !ok {
return nil, errors.New("COOKIEFILE not found in PROTOCOLINFO " +
"reply")
}
cookieFilePath = strings.Trim(cookieFilePath, "\"")
// Read the cookie from the file and ensure it has the correct length.
cookie, err := ioutil.ReadFile(cookieFilePath)
@ -360,48 +408,36 @@ func supportsV3(version string) error {
return nil
}
// ProtocolInfo returns the different authentication methods supported by the
// Tor server and the version of the Tor server.
func (c *Controller) ProtocolInfo() ([]string, string, string, error) {
// We'll start off by sending the "PROTOCOLINFO" command to the Tor
// server. We should receive a reply of the following format:
//
// METHODS=COOKIE,SAFECOOKIE
// COOKIEFILE="/home/user/.tor/control_auth_cookie"
// VERSION Tor="0.3.2.10"
//
// We're interested in retrieving all of these fields, so we'll parse
// our reply to do so.
// protocolInfo is encompasses the details of a response to a PROTOCOLINFO
// command.
type protocolInfo map[string]string
// version returns the Tor version as reported by the server.
func (i protocolInfo) version() string {
version := i["Tor"]
return strings.Trim(version, "\"")
}
// supportsAuthMethod determines whether the Tor server supports the given
// 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)
_, reply, err := c.sendCommand(cmd)
if err != nil {
return nil, "", "", err
return nil, err
}
info := parseTorReply(reply)
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
return protocolInfo(parseTorReply(reply)), nil
}
// OnionType denotes the type of the onion service.