What changed, and why it matters
This commit fixes three security issues in how Eclair connects to the Tor network and stores sensitive files. First, it changes the default Tor authentication from password to safecookie, and blocks password mode when the Tor control port is on a remote machine, because password mode sends secrets in cleartext and cannot prove the server is really Tor. Second, it checks that Tor's authentication cookie is exactly 32 bytes. Third, it fixes a race condition where secret files (seed files and Tor private keys) were created with loose permissions before being locked down, which could let other users on the same machine read them; now the file is created with strict permissions first, then the secret is written.
Users running Tor with Eclair should upgrade, switch to safecookie authentication, ensure the Tor control port is not exposed remotely, and verify that existing seed and tor.dat files have restrictive owner-only permissions. Operators should review Tor daemon configuration and firewall rules for the control port.
Security signals we found
Default authentication changed from password to safecookie
Password authentication rejected for remote Tor control ports
Tor cookie length validated to be exactly 32 bytes
File permission race condition fixed for seed and Tor private key files
Documentation updated to warn about remote Tor control port risks
Evidence from the diff
The patch addresses: (1) authentication downgrade/impersonation risk by defaulting to safecookie and rejecting Password authentication when the Tor control address is not loopback, since the Tor control protocol with password auth is cleartext and unauthenticated; (2) input validation by enforcing a 32-byte cookie length in computeClientHash; (3) a TOCTOU file-permission race in seed and Tor private-key creation by introducing createSecretFile/writeSecret helpers that atomically create an empty file with POSIX owner-only permissions before writing content. The changes span NodeParams.scala, package.scala, TorProtocolHandler.scala, reference.conf, docs, and tests.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scalaeclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scalaeclair-core/src/main/scala/fr/acinq/eclair/package.scalaeclair-core/src/main/resources/reference.confdocs/Tor.mddocs/release-notes/eclair-vnext.mdInspect captured patch +171 / −106
### docs/Tor.md
@@ -4,25 +4,25 @@ Current supported version of Tor is 0.3.3.6 or higher.
### Installing Tor on your node
-#### Linux:
+#### Linux
```shell
sudo apt install tor
```
-#### Mac OS X:
+#### Mac OS X
```shell
brew install tor
```
-#### Windows:
+#### Windows
[Download the "Expert Bundle"](https://www.torproject.org/download/tor/) from Tor's website and extract it to `C:\tor`.
### Configuring Tor
-#### Linux and Max OS X:
+#### Linux and Max OS X
Eclair requires safe cookie authentication as well as SOCKS5 and control connections to be enabled.
@@ -37,7 +37,7 @@ ExitPolicy reject *:* # don't change this unless you really know what you are do
Make sure eclair is allowed to read Tor's cookie file (typically `/var/run/tor/control.authcookie`).
-#### Windows:
+#### Windows
On Windows, it is easier to use the password authentication mechanism.
@@ -60,19 +60,19 @@ ExitPolicy reject *:* # don't change this unless you really know what you are do
### Start Tor
-#### Linux:
+#### Linux
```shell
sudo systemctl start tor
```
-#### Mac OS X:
+#### Mac OS X
```shell
brew services start tor
```
-#### Windows:
+#### Windows
Open a CMD with administrator access
@@ -84,6 +84,7 @@ tor --service install -options -f "c:\tor\Conf\torrc"
### Configure Tor hidden service
To create a Tor hidden service endpoint simply set the `eclair.tor.enabled` parameter in `eclair.conf` to true.
+
```
eclair.tor.enabled = true
```
@@ -94,16 +95,25 @@ eclair.tor.auth = safecookie
# eclair.tor.password = "" # Needed if you set auth to password
```
+:warning: The Tor control port is a trusted local interface and must not be exposed to other machines. Eclair sends its
+onion private key to the control port on every startup, so anything that can read that connection can impersonate your
+node's onion address. With `password` authentication, the Tor control protocol also gives eclair no way of verifying
+that it is really talking to Tor: the password is sent in cleartext to whatever process is listening on the control
+port. Eclair therefore refuses to use `password` authentication unless `eclair.tor.host` is a local address. If your Tor
+daemon runs on another host or in a separate container, use `safecookie`, which authenticates the Tor server.
+
Eclair will automatically set up a hidden service endpoint and add its onion address to the `server.public-ips` list.
You can see what onion address is assigned using `eclair-cli`:
```shell
eclair-cli getinfo
```
-Eclair saves the Tor endpoint's private key in `~/.eclair/tor.dat`, so that it can recreate the endpoint address after
-a restart. If you remove the private key Eclair will regenerate the endpoint address.
-
+
+Eclair saves the Tor endpoint's private key in `~/.eclair/tor.dat`, so that it can recreate the endpoint address after
+a restart. If you remove the private key Eclair will regenerate the endpoint address.
+
For increased privacy do not advertise your IP address in the `server.public-ips` list, and set your binding IP to `localhost`:
+
```
eclair.server.binding-ip = "127.0.0.1"
```
@@ -118,11 +128,13 @@ You can always see your node's onion address using `getinfo` CLI command.
### Configure SOCKS5 proxy
-By default, all incoming connections will be established via Tor network, but all outgoing will be created via the
+By default, all incoming connections will be established via Tor network, but all outgoing will be created via the
clearnet. To route them through Tor you can use Tor's SOCKS5 proxy. Add this line in your `eclair.conf`:
+
```
eclair.socks5.enabled = true
```
+
You can use SOCKS5 proxy only for specific types of addresses. Use `eclair.socks5.use-for-ipv4`, `eclair.socks5.use-for-ipv6`
or `eclair.socks5.use-for-tor` for fine-tuning.
@@ -132,14 +144,14 @@ To create a new Tor circuit for every connection, use `randomize-credentials` pa
eclair.socks5.randomize-credentials = true
```
-:warning: Tor hidden service and SOCKS5 are independent options. You can use just one of them, but if you want to get most privacy
+:warning: Tor hidden service and SOCKS5 are independent options. You can use just one of them, but if you want to get most privacy
features from using Tor, use both.
Note, that bitcoind should be configured to use Tor as well (https://en.bitcoin.it/wiki/Setting_up_a_Tor_hidden_service).
### Blockchain watchdogs
-Eclair version 0.5.0 introduced blockchain watchdogs, that fetch bitcoin headers from various sources in
+Eclair version 0.5.0 introduced blockchain watchdogs, that fetch bitcoin headers from various sources in
order to detect whether the node is being eclipsed. Eclair supports four sources at the moment:
* blockchainheaders.net
@@ -166,6 +178,6 @@ eclair.blockchain-watchdog.sources = [
Also, you can disable Tor for all watchdog sources altogether using:
-```s
+```
eclair.socks5.use-for-watchdogs = false
```
\ No newline at end of file
### docs/release-notes/eclair-vnext.md
@@ -28,6 +28,8 @@ eclair.features.option_onion_messages_only_channels = optional
### Configuration changes
+#### Gossip queries
+
A `query_channel_range` message is a few dozen bytes to send, but answering one requires scanning our whole routing
table and sending back megabytes of data. We now limit how many of them we accept from a given peer, which can be
configured with:
@@ -49,6 +51,17 @@ eclair.router.sync.max-queries-per-sync = 2000
At the default `channel-query-chunk-size` this covers 200 000 channels, which is several times the current size of the
network.
+#### Tor configuration
+
+`eclair.tor.auth` now defaults to `safecookie` instead of `password`.
+
+Password authentication sends our tor control password in cleartext to whatever process is listening on the control
+port, without any way of verifying that it is really tor. It is now rejected when `eclair.tor.host` isn't a local
+address: if you run tor on another host or in a separate container, switch to `eclair.tor.auth = safecookie`, which
+authenticates the tor server, or move the control port to the host running eclair. Note that the onion private key is
+sent to the control port on every startup, so a remote control port exposes it to the network regardless of the
+authentication method used.
+
### API changes
<insert changes>
### eclair-core/src/main/resources/reference.conf
@@ -551,9 +551,9 @@ eclair {
tor {
enabled = false
- auth = "password" // safecookie, password
- password = "foobar" // used when auth=password
- host = "127.0.0.1"
+ auth = "safecookie" // safecookie, password
+ password = "foobar" // used when auth=password, which requires host to be a local address
+ host = "127.0.0.1" // must be a local address unless auth=safecookie
port = 9051
private-key-file = "tor.dat"
targets = [] // a list of address:port, for advanced use (e.g. to send traffic to front servers). See the tor man page for syntax details.
### eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
@@ -211,17 +211,14 @@ object NodeParams extends Logging {
}
private def writeSeedToFile(path: File, seed: ByteVector): Unit = {
- Files.write(path.toPath, seed.toArray)
- // Seed files derive the node identity key and all channel keys: they must never be readable by other local users.
- setOwnerPermissions(path.toPath, "rw-------")
+ writeSecret(path.toPath, seed.toArray)
logger.info(s"create new seed file: ${path.getCanonicalPath}")
}
private def migrateSeedFile(source: File, destination: File): Unit = {
if (source.exists() && !destination.exists()) {
- Files.copy(source.toPath, destination.toPath)
- // The copied file inherits default umask permissions, so we restrict it to its owner like a freshly created seed.
- setOwnerPermissions(destination.toPath, "rw-------")
+ val seed = readSeedFromFile(source)
+ writeSecret(destination.toPath, seed.toArray)
logger.info(s"migrate seed file: ${source.getCanonicalPath} → ${destination.getCanonicalPath}")
}
}
### eclair-core/src/main/scala/fr/acinq/eclair/package.scala
@@ -24,24 +24,54 @@ import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import scodec.Attempt
import scodec.bits.{BitVector, ByteVector}
-import java.nio.file.Path
import java.nio.file.attribute.PosixFilePermissions
+import java.nio.file.{Files, Path}
package object eclair {
val randomGen = new StrongRandom()
+ /**
+ * Create an empty file that only the current user can read, replacing it if it already exists. The permissions must
+ * be set when the file is created: if we set them after writing to the file, there is a window during which other
+ * users can read its content.
+ */
+ def createSecretFile(path: Path): Unit = {
+ // File attributes are ignored when the file already exists, so we start from a clean slate.
+ Files.deleteIfExists(path)
+ try {
+ // The permissions are passed to the underlying open(2) call, so the file never exists with broader permissions.
+ // This also fails if someone else concurrently created that file, instead of storing our secret in their file.
+ Files.createFile(path, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")))
+ } catch {
+ case _: UnsupportedOperationException => Files.createFile(path) // non-POSIX file system (e.g. Windows)
+ }
+ }
+
+ /** Write a secret to a file that only the current user can read. */
+ def writeSecret(path: Path, secret: String): Unit = {
+ createSecretFile(path)
+ Files.writeString(path, secret) // the file already exists, so this doesn't change its permissions
+ }
+
+ /** Write a secret to a file that only the current user can read. */
+ def writeSecret(path: Path, secret: Array[Byte]): Unit = {
+ createSecretFile(path)
+ Files.write(path, secret) // the file already exists, so this doesn't change its permissions
+ }
+
/**
* Restrict access to a file or directory to its owner, e.g. "rw-------" for a secret file or "rwx------" for a
* directory containing secrets. This is a no-op on file systems that don't support POSIX permissions (e.g. Windows),
* where access control is handled differently.
*/
- def setOwnerPermissions(path: Path, permissions: String): Unit =
+ def setOwnerPermissions(path: Path, permissions: String): Unit = {
try {
- java.nio.file.Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissions))
+ Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissions))
} catch {
case _: UnsupportedOperationException => () // non-POSIX file system (e.g. Windows)
}
+ }
def randomBytes(length: Int): ByteVector = {
val buffer = new Array[Byte](length)
### eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala
@@ -21,10 +21,11 @@ import akka.io.Tcp.Connected
import akka.util.ByteString
import fr.acinq.eclair.tor.TorProtocolHandler.Authentication
import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor3}
+import fr.acinq.eclair.writeSecret
import scodec.bits.Bases.Alphabets
import scodec.bits.ByteVector
-import java.nio.file.attribute.PosixFilePermissions
+import java.net.InetSocketAddress
import java.nio.file.{Files, Path, Paths}
import java.util
import javax.crypto.Mac
@@ -35,22 +36,21 @@ import scala.util.Try
case class TorException(private val msg: String) extends RuntimeException(s"Tor error: $msg")
/**
- * Created by rorp
- *
- * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt
- *
- * @param authentication Tor controller auth mechanism (password or safecookie)
- * @param privateKeyPath path to a file that contains a Tor private key
- * @param virtualPort port for the public hidden service (typically 9735)
- * @param targets address of our protected server (format [host:]port), 127.0.0.1:[[virtualPort]] if empty
- * @param onionAdded a Promise to track creation of the endpoint
- */
+ * Created by rorp
+ *
+ * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt
+ *
+ * @param authentication Tor controller auth mechanism (password or safecookie)
+ * @param privateKeyPath path to a file that contains a Tor private key
+ * @param virtualPort port for the public hidden service (typically 9735)
+ * @param targets address of our protected server (format [host:]port), 127.0.0.1:[[virtualPort]] if empty
+ * @param onionAdded a Promise to track creation of the endpoint
+ */
class TorProtocolHandler(authentication: Authentication,
privateKeyPath: Path,
virtualPort: Int,
targets: Seq[String],
- onionAdded: Option[Promise[NodeAddress]]
- ) extends Actor with Stash with ActorLogging {
+ onionAdded: Option[Promise[NodeAddress]]) extends Actor with Stash with ActorLogging {
import TorProtocolHandler._
@@ -59,12 +59,28 @@ class TorProtocolHandler(authentication: Authentication,
private var address: Option[NodeAddress] = None
override def receive: Receive = {
- case Connected(_, _) =>
+ case Connected(remoteAddress, _) =>
+ checkControlAddress(remoteAddress)
receiver = sender()
sendCommand("PROTOCOLINFO 1")
context become protocolInfo
}
+ /**
+ * The tor control protocol doesn't let us authenticate the server when using password authentication: we would send
+ * our password in cleartext to whatever process is listening on the control port. And since ADD_ONION also sends our
+ * onion private key in cleartext, a remote control port exposes both secrets to the network. We thus only allow
+ * password authentication on a local control port, and require safecookie otherwise, which authenticates the server.
+ */
+ private def checkControlAddress(remoteAddress: InetSocketAddress): Unit = {
+ val isLocal = Option(remoteAddress.getAddress).exists(_.isLoopbackAddress)
+ authentication match {
+ case _: Password if !isLocal => throw TorException(s"cannot use password authentication with a remote control port ($remoteAddress): use safecookie instead")
+ case _ if !isLocal => log.warning("tor control port {} is not local: our onion private key will be sent in cleartext over the network", remoteAddress)
+ case _ => ()
+ }
+ }
+
def protocolInfo: Receive = {
case data: ByteString =>
val res = parseResponse(readResponse(data))
@@ -134,10 +150,7 @@ class TorProtocolHandler(authentication: Authentication,
private def processOnionResponse(res: Map[String, String]): String = {
val serviceId = res.getOrElse("ServiceID", throw TorException("service ID not found"))
val privateKey = res.get("PrivateKey")
- privateKey.foreach { pk =>
- writeString(privateKeyPath, pk)
- setPermissions(privateKeyPath, "rw-------")
- }
+ privateKey.foreach(pk => writeSecret(privateKeyPath, pk))
serviceId
}
@@ -158,15 +171,19 @@ class TorProtocolHandler(authentication: Authentication,
}
private def computeClientHash(serverHash: ByteVector, serverNonce: ByteVector, clientNonce: ByteVector, cookieFile: Path): ByteVector = {
- if (serverHash.length != 32)
+ if (serverHash.length != 32) {
throw TorException("invalid server hash length")
- if (serverNonce.length != 32)
+ }
+ if (serverNonce.length != 32) {
throw TorException("invalid server nonce length")
+ }
val cookie = ByteVector.view(Files.readAllBytes(cookieFile))
+ if (cookie.length != 32) {
+ throw TorException("invalid server cookie length")
+ }
val message = cookie ++ clientNonce ++ serverNonce
-
val computedServerHash = hmacSHA256(ServerKey, message)
if (computedServerHash != serverHash) {
throw TorException("unexpected server hash")
@@ -226,13 +243,6 @@ object TorProtocolHandler {
def writeString(path: Path, string: String): Unit = Files.write(path, util.Arrays.asList(string))
- def setPermissions(path: Path, permissionString: String): Unit =
- try {
- Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissionString))
- } catch {
- case _: UnsupportedOperationException => () // we are on windows
- }
-
def unquote(s: String): String = s
.stripSuffix("\"")
.stripPrefix("\"")
### eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala
@@ -16,19 +16,18 @@
package fr.acinq.eclair.tor
-import java.net.InetSocketAddress
-import java.nio.file.{Files, Paths}
-
-import akka.actor.ActorNotFound
import akka.io.Tcp.Connected
import akka.testkit.{ImplicitSender, TestActorRef}
import akka.util.ByteString
-import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor2, Tor3}
-import fr.acinq.eclair.{TestKitBaseClass, TestUtils}
-import org.scalatest._
+import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor3}
+import fr.acinq.eclair.{TestKitBaseClass, TestUtils, createSecretFile, writeSecret}
+import org.scalatest.Outcome
import org.scalatest.funsuite.AnyFunSuiteLike
import scodec.bits._
+import java.net.InetSocketAddress
+import java.nio.file.attribute.PosixFilePermissions
+import java.nio.file.{FileSystems, Files, Paths}
import scala.concurrent.duration._
import scala.concurrent.{Await, Promise}
@@ -39,94 +38,125 @@ class TorProtocolHandlerSpec extends TestKitBaseClass
import TorProtocolHandler._
val LocalHost = new InetSocketAddress("localhost", 8888)
+ val RemoteHost = new InetSocketAddress("1.2.3.4", 8888)
val PASSWORD = "foobar"
val ClientNonce = hex"8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA"
val PkFilePath = Paths.get(TestUtils.BUILD_DIRECTORY, "testtorpk.dat")
val CookieFilePath = Paths.get(TestUtils.BUILD_DIRECTORY, "testtorcookie.dat")
val AuthCookie = hex"AA8593C52DF9713CC5FF6A1D0A045B3FADCAE57745B1348A62A6F5F88D940485"
- override def withFixture(test: NoArgTest) = {
+ override def withFixture(test: NoArgTest): Outcome = {
PkFilePath.toFile.delete()
super.withFixture(test) // Invoke the test function
}
ignore("connect to real tor daemon") {
val promiseOnionAddress = Promise[NodeAddress]()
-
val protocolHandlerProps = TorProtocolHandler.props(
authentication = Password(PASSWORD),
privateKeyPath = PkFilePath,
virtualPort = 9999,
onionAdded = Some(promiseOnionAddress))
-
val controller = TestActorRef(Controller.props(new InetSocketAddress("localhost", 9051), protocolHandlerProps), "tor")
-
val address = Await.result(promiseOnionAddress.future, 30 seconds)
println(address)
}
test("happy path v2") {
val promiseOnionAddress = Promise[NodeAddress]()
-
val protocolHandler = TestActorRef(props(
authentication = Password(PASSWORD),
privateKeyPath = PkFilePath,
virtualPort = 9999,
onionAdded = Some(promiseOnionAddress)))
-
protocolHandler ! Connected(LocalHost, LocalHost)
-
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
protocolHandler ! ByteString(
"250-PROTOCOLINFO 1\r\n" +
"250-AUTH METHODS=HASHEDPASSWORD\r\n" +
"250-VERSION Tor=\"0.3.3.5\"\r\n" +
"250 OK\r\n"
)
-
awaitCond(promiseOnionAddress.isCompleted)
-
assertThrows[TorException](Await.result(promiseOnionAddress.future, Duration.Inf))
}
test("happy path v3") {
val promiseOnionAddress = Promise[NodeAddress]()
-
val protocolHandler = TestActorRef(props(
authentication = Password(PASSWORD),
privateKeyPath = PkFilePath,
virtualPort = 9999,
onionAdded = Some(promiseOnionAddress)))
-
protocolHandler ! Connected(LocalHost, LocalHost)
-
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
protocolHandler ! ByteString(
"250-PROTOCOLINFO 1\r\n" +
"250-AUTH METHODS=HASHEDPASSWORD\r\n" +
"250-VERSION Tor=\"0.3.4.8\"\r\n" +
"250 OK\r\n"
)
-
expectMsg(ByteString(s"""AUTHENTICATE "$PASSWORD"\r\n"""))
protocolHandler ! ByteString(
"250 OK\r\n"
)
-
expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n"))
protocolHandler ! ByteString(
"250-ServiceID=mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd\r\n" +
"250-PrivateKey=ED25519-V3:private-key\r\n" +
"250 OK\r\n"
)
-
protocolHandler ! GetOnionAddress
expectMsg(Some(Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)))
-
val address = Await.result(promiseOnionAddress.future, 3 seconds)
assert(address == Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999))
+ assert(readString(PkFilePath) == "ED25519-V3:private-key")
+ }
+
+ test("reject password authentication on a remote control port") {
+ val promiseOnionAddress = Promise[NodeAddress]()
+ val protocolHandler = TestActorRef(props(
+ authentication = Password(PASSWORD),
+ privateKeyPath = PkFilePath,
+ virtualPort = 9999,
+ onionAdded = Some(promiseOnionAddress)))
+ protocolHandler ! Connected(RemoteHost, LocalHost)
+ // we don't send anything to a control port that we cannot authenticate
+ expectNoMessage(100 millis)
+ assert(intercept[TorException] {
+ Await.result(promiseOnionAddress.future, 3 seconds)
+ } == TorException(s"cannot use password authentication with a remote control port ($RemoteHost): use safecookie instead"))
+ }
+
+ test("allow safecookie authentication on a remote control port") {
+ val promiseOnionAddress = Promise[NodeAddress]()
+ val protocolHandler = TestActorRef(props(
+ authentication = SafeCookie(ClientNonce),
+ privateKeyPath = PkFilePath,
+ virtualPort = 9999,
+ onionAdded = Some(promiseOnionAddress)))
+ protocolHandler ! Connected(RemoteHost, LocalHost)
+ // safecookie lets us verify that we're talking to the real tor server, so we allow a remote control port
+ expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
+ }
+ test("create the private key file with restricted permissions") {
+ assume(FileSystems.getDefault.supportedFileAttributeViews().contains("posix"), "this test requires a posix file system")
+ // the private key file may already exist with permissions that are too broad, for example if it was created by a
+ // previous version of eclair or restored from a backup
+ writeString(PkFilePath, "ED25519-V3:previous-key")
+ Files.setPosixFilePermissions(PkFilePath, PosixFilePermissions.fromString("rw-rw-rw-"))
+ createSecretFile(PkFilePath)
+ // the permissions are already restricted while the file is still empty: our private key is never readable by others
+ assert(Files.size(PkFilePath) == 0)
+ assert(PosixFilePermissions.toString(Files.getPosixFilePermissions(PkFilePath)) == "rw-------")
+ }
+
+ test("write private key to a file that only we can read") {
+ assume(FileSystems.getDefault.supportedFileAttributeViews().contains("posix"), "this test requires a posix file system")
+ writeSecret(PkFilePath, "ED25519-V3:private-key")
assert(readString(PkFilePath) == "ED25519-V3:private-key")
+ assert(PosixFilePermissions.toString(Files.getPosixFilePermissions(PkFilePath)) == "rw-------")
}
test("v2/v3 compatibility check against tor version") {
@@ -140,136 +170,109 @@ class TorProtocolHandlerSpec extends TestKitBaseClass
test("authentication method errors") {
val promiseOnionAddress = Promise[NodeAddress]()
-
val protocolHandler = TestActorRef(props(
authentication = Password(PASSWORD),
privateKeyPath = PkFilePath,
virtualPort = 9999,
onionAdded = Some(promiseOnionAddress)))
-
protocolHandler ! Connected(LocalHost, LocalHost)
-
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
protocolHandler ! ByteString(
"250-PROTOCOLINFO 1\r\n" +
"250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" +
"250-VERSION Tor=\"0.3.3.6\"\r\n" +
"250 OK\r\n"
)
-
assert(intercept[TorException] {
Await.result(promiseOnionAddress.future, 3 seconds)
} == TorException("cannot use authentication 'password', supported methods are 'COOKIE,SAFECOOKIE'"))
}
test("invalid server hash") {
val promiseOnionAddress = Promise[NodeAddress]()
-
Files.write(CookieFilePath, fr.acinq.eclair.randomBytes32().toArray)
-
val protocolHandler = TestActorRef(props(
authentication = SafeCookie(ClientNonce),
privateKeyPath = PkFilePath,
virtualPort = 9999,
onionAdded = Some(promiseOnionAddress)))
-
protocolHandler ! Connected(LocalHost, LocalHost)
-
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
protocolHandler ! ByteString(
"250-PROTOCOLINFO 1\r\n" +
"250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" +
"250-VERSION Tor=\"0.3.3.6\"\r\n" +
"250 OK\r\n"
)
-
expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n"))
protocolHandler ! ByteString(
"250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n"
)
-
assert(intercept[TorException] {
Await.result(promiseOnionAddress.future, 3 seconds)
} == TorException("unexpected server hash"))
}
-
test("AUTHENTICATE failure") {
val promiseOnionAddress = Promise[NodeAddress]()
-
Files.write(CookieFilePath, AuthCookie.toArray)
-
val protocolHandler = TestActorRef(props(
authentication = SafeCookie(ClientNonce),
privateKeyPath = PkFilePath,
virtualPort = 9999,
onionAdded = Some(promiseOnionAddress)))
-
protocolHandler ! Connected(LocalHost, LocalHost)
-
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
protocolHandler ! ByteString(
"250-PROTOCOLINFO 1\r\n" +
"250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" +
"250-VERSION Tor=\"0.3.3.6\"\r\n" +
"250 OK\r\n"
)
-
expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n"))
protocolHandler ! ByteString(
"250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n"
)
-
expectMsg(ByteString("AUTHENTICATE 0ddcab5deb39876cdef7af7860a1c738953395349f43b99f4e5e0f131b0515df\r\n"))
protocolHandler ! ByteString(
"515 Authentication failed: Safe cookie response did not match expected value.\r\n"
)
-
assert(intercept[TorException] {
Await.result(promiseOnionAddress.future, 3 seconds)
} == TorException("server returned error: 515 Authentication failed: Safe cookie response did not match expected value."))
}
test("ADD_ONION failure") {
val promiseOnionAddress = Promise[NodeAddress]()
-
Files.write(CookieFilePath, AuthCookie.toArray)
-
val protocolHandler = TestActorRef(props(
authentication = SafeCookie(ClientNonce),
privateKeyPath = PkFilePath,
virtualPort = 9999,
onionAdded = Some(promiseOnionAddress)))
-
protocolHandler ! Connected(LocalHost, LocalHost)
-
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
protocolHandler ! ByteString(
"250-PROTOCOLINFO 1\r\n" +
"250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" +
"250-VERSION Tor=\"0.3.3.6\"\r\n" +
"250 OK\r\n"
)
-
expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n"))
protocolHandler ! ByteString(
"250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n"
)
-
expectMsg(ByteString("AUTHENTICATE 0ddcab5deb39876cdef7af7860a1c738953395349f43b99f4e5e0f131b0515df\r\n"))
protocolHandler ! ByteString(
"250 OK\r\n"
)
-
expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n"))
protocolHandler ! ByteString(
"513 Invalid argument\r\n"
)
-
- val t = intercept[TorException] {
+ intercept[TorException] {
Await.result(promiseOnionAddress.future, 3 seconds)
}
-
assert(intercept[TorException] {
Await.result(promiseOnionAddress.future, 3 seconds)
} == TorException("server returned error: 513 Invalid argument"))Why this scored 67/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.