lnd/tor/controller.go
Olaoluwa Osuntokun 4cb518c174
Merge pull request #4056 from wpaulino/tor-onion-store
tor+server: add OnionStore to AddOnionConfig with file-based implementation
2020-03-12 18:49:57 -07:00

441 lines
14 KiB
Go

package tor
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"net/textproto"
"strconv"
"strings"
"sync/atomic"
)
const (
// success is the Tor Control response code representing a successful
// request.
success = 250
// nonceLen is the length of a nonce generated by either the controller
// or the Tor server
nonceLen = 32
// cookieLen is the length of the authentication cookie.
cookieLen = 32
// ProtocolInfoVersion is the `protocolinfo` version currently supported
// by the Tor server.
ProtocolInfoVersion = 1
// MinTorVersion is the minimum supported version that the Tor server
// 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 (
// serverKey is the key used when computing the HMAC-SHA256 of a message
// from the server.
serverKey = []byte("Tor safe cookie authentication " +
"server-to-controller hash")
// controllerKey is the key used when computing the HMAC-SHA256 of a
// message from the controller.
controllerKey = []byte("Tor safe cookie authentication " +
"controller-to-server hash")
)
// Controller is an implementation of the Tor Control protocol. This is used in
// order to communicate with a Tor server. Its only supported method of
// authentication is the SAFECOOKIE method.
//
// NOTE: The connection to the Tor server must be authenticated before
// proceeding to send commands. Otherwise, the connection will be closed.
//
// TODO:
// * if adding support for more commands, extend this with a command queue?
// * place under sub-package?
// * support async replies from the server
type Controller struct {
// started is used atomically in order to prevent multiple calls to
// Start.
started int32
// stopped is used atomically in order to prevent multiple calls to
// Stop.
stopped int32
// conn is the underlying connection between the controller and the
// Tor server. It provides read and write methods to simplify the
// text-based messages within the connection.
conn *textproto.Conn
// controlAddr is the host:port the Tor server is listening locally for
// 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
// 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,
password string) *Controller {
return &Controller{
controlAddr: controlAddr,
targetIPAddress: targetIPAddress,
password: password,
}
}
// Start establishes and authenticates the connection between the controller and
// a Tor server. Once done, the controller will be able to send commands and
// expect responses.
func (c *Controller) Start() error {
if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
return nil
}
conn, err := textproto.Dial("tcp", c.controlAddr)
if err != nil {
return fmt.Errorf("unable to connect to Tor server: %v", err)
}
c.conn = conn
return c.authenticate()
}
// Stop closes the connection between the controller and the Tor server.
func (c *Controller) Stop() error {
if !atomic.CompareAndSwapInt32(&c.stopped, 0, 1) {
return nil
}
return c.conn.Close()
}
// sendCommand sends a command to the Tor server and returns its response, as a
// single space-delimited string, and code.
func (c *Controller) sendCommand(command string) (int, string, error) {
if err := c.conn.Writer.PrintfLine(command); err != nil {
return 0, "", err
}
// We'll use ReadResponse as it has built-in support for multi-line
// text protocol responses.
code, reply, err := c.conn.Reader.ReadResponse(success)
if err != nil {
return code, reply, err
}
return code, reply, nil
}
// parseTorReply parses the reply from the Tor server after receiving a command
// from a controller. This will parse the relevant reply parameters into a map
// of keys and values.
func parseTorReply(reply string) map[string]string {
params := make(map[string]string)
// Replies can either span single or multiple lines, so we'll default
// to stripping whitespace and newlines in order to retrieve the
// individual contents of it. The -1 indicates that we want this to span
// across all instances of a newline.
contents := strings.Split(strings.Replace(reply, "\n", " ", -1), " ")
for _, content := range contents {
// Each parameter within the reply should be of the form
// "KEY=VALUE". If the parameter doesn't contain "=", then we
// can assume it does not provide any other relevant information
// already known.
keyValue := strings.SplitN(content, "=", 2)
if len(keyValue) != 2 {
continue
}
key := keyValue[0]
value := keyValue[1]
params[key] = value
}
return params
}
// authenticate authenticates the connection between the controller and the
// 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(info)
if err != nil {
return fmt.Errorf("unable to retrieve authentication cookie: "+
"%v", 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.
clientNonce := make([]byte, nonceLen)
if _, err := rand.Read(clientNonce); err != nil {
return fmt.Errorf("unable to generate client nonce: %v", err)
}
cmd := fmt.Sprintf("AUTHCHALLENGE SAFECOOKIE %x", clientNonce)
_, reply, err := c.sendCommand(cmd)
if err != nil {
return err
}
// If successful, the reply from the server should be of the following
// format:
//
// "250 AUTHCHALLENGE"
// SP "SERVERHASH=" ServerHash
// SP "SERVERNONCE=" ServerNonce
// CRLF
//
// We're interested in retrieving the SERVERHASH and SERVERNONCE
// parameters, so we'll parse our reply to do so.
replyParams := parseTorReply(reply)
// Once retrieved, we'll ensure these values are of proper length when
// decoded.
serverHash, ok := replyParams["SERVERHASH"]
if !ok {
return errors.New("server hash not found in reply")
}
decodedServerHash, err := hex.DecodeString(serverHash)
if err != nil {
return fmt.Errorf("unable to decode server hash: %v", err)
}
if len(decodedServerHash) != sha256.Size {
return errors.New("invalid server hash length")
}
serverNonce, ok := replyParams["SERVERNONCE"]
if !ok {
return errors.New("server nonce not found in reply")
}
decodedServerNonce, err := hex.DecodeString(serverNonce)
if err != nil {
return fmt.Errorf("unable to decode server nonce: %v", err)
}
if len(decodedServerNonce) != nonceLen {
return errors.New("invalid server nonce length")
}
// The server hash above was constructed by computing the HMAC-SHA256
// of the message composed of the cookie, client nonce, and server
// nonce. We'll redo this computation ourselves to ensure the integrity
// and authentication of the message.
hmacMessage := bytes.Join(
[][]byte{cookie, clientNonce, decodedServerNonce}, []byte{},
)
computedServerHash := computeHMAC256(serverKey, hmacMessage)
if !hmac.Equal(computedServerHash, decodedServerHash) {
return fmt.Errorf("expected server hash %x, got %x",
decodedServerHash, computedServerHash)
}
// If the MAC check was successful, we'll proceed with the last step of
// the authentication routine. We'll now send the AUTHENTICATE command
// followed by a hex-encoded client hash constructed by computing the
// HMAC-SHA256 of the same message, but this time using the controller's
// key.
clientHash := computeHMAC256(controllerKey, hmacMessage)
if len(clientHash) != sha256.Size {
return errors.New("invalid client hash length")
}
cmd = fmt.Sprintf("AUTHENTICATE %x", clientHash)
if _, _, err := c.sendCommand(cmd); err != nil {
return err
}
return nil
}
// getAuthCookie retrieves the authentication cookie in bytes from the Tor
// 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)
if err != nil {
return nil, err
}
if len(cookie) != cookieLen {
return nil, errors.New("invalid authentication cookie length")
}
return cookie, nil
}
// computeHMAC256 computes the HMAC-SHA256 of a key and message.
func computeHMAC256(key, message []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write(message)
return mac.Sum(nil)
}
// supportsV3 is a helper function that parses the current version of the Tor
// server and determines whether it supports creationg v3 onion services through
// Tor's control port. The version string should be of the format:
// major.minor.revision.build
func supportsV3(version string) error {
// We'll split the minimum Tor version that's supported and the given
// version in order to individually compare each number.
parts := strings.Split(version, ".")
if len(parts) != 4 {
return errors.New("version string is not of the format " +
"major.minor.revision.build")
}
// It's possible that the build number (the last part of the version
// string) includes a pre-release string, e.g. rc, beta, etc., so we'll
// parse that as well.
build := strings.Split(parts[len(parts)-1], "-")
parts[len(parts)-1] = build[0]
// Ensure that each part of the version string corresponds to a number.
for _, part := range parts {
if _, err := strconv.Atoi(part); err != nil {
return err
}
}
// Once we've determined we have a proper version string of the format
// major.minor.revision.build, we can just do a string comparison to
// determine if it satisfies the minimum version supported.
if version < MinTorVersion {
return fmt.Errorf("version %v below minimum version supported "+
"%v", version, MinTorVersion)
}
return nil
}
// 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 protocolInfo(parseTorReply(reply)), nil
}