Skip to content

Commit

Permalink
Merge pull request from GHSA-2cpx-6pqp-wf35
Browse files Browse the repository at this point in the history
Verify client certificate when `requestCert=true` for server-mode `TLSSocket` on Node.js
  • Loading branch information
ChristopherDavenport committed Jul 26, 2022
2 parents 6bff27d + 47bced4 commit 19ce392
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 11 deletions.
3 changes: 3 additions & 0 deletions io/js/src/main/scala/fs2/io/internal/facade/events.scala
Expand Up @@ -38,6 +38,9 @@ private[io] trait EventEmitter extends js.Object {
protected[io] def on[E, F](eventName: String, listener: js.Function2[E, F, Unit]): this.type =
js.native

protected[io] def once(eventName: String, listener: js.Function0[Unit]): this.type =
js.native

protected[io] def once[E](eventName: String, listener: js.Function1[E, Unit]): this.type =
js.native

Expand Down
7 changes: 7 additions & 0 deletions io/js/src/main/scala/fs2/io/internal/facade/tls.scala
Expand Up @@ -189,6 +189,13 @@ package tls {

def alpnProtocol: String | Boolean = js.native

def ssl: SSL = js.native

}

@js.native
private[io] trait SSL extends js.Object {
def verifyError(): js.Error = js.native
}

}
39 changes: 31 additions & 8 deletions io/js/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala
Expand Up @@ -30,6 +30,8 @@ import cats.effect.std.Dispatcher
import cats.syntax.all._
import fs2.io.internal.facade

import scala.scalajs.js

private[tls] trait TLSContextPlatform[F[_]]

private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type =>
Expand Down Expand Up @@ -69,14 +71,35 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type =>
}
)
} else {
val options = params.toTLSSocketOptions(dispatcher)
options.secureContext = context
options.enableTrace = logger != TLSLogger.Disabled
options.isServer = true
TLSSocket.forAsync(
socket,
sock => new facade.tls.TLSSocket(sock, options)
)
Resource.eval(F.deferred[Either[Throwable, Unit]]).flatMap { verifyError =>
TLSSocket
.forAsync(
socket,
sock => {
val options = params.toTLSSocketOptions(dispatcher)
options.secureContext = context
options.enableTrace = logger != TLSLogger.Disabled
options.isServer = true
val tlsSock = new facade.tls.TLSSocket(sock, options)
tlsSock.once(
"secure",
{ () =>
val requestCert = options.requestCert.getOrElse(false)
val rejectUnauthorized = options.rejectUnauthorized.getOrElse(true)
val result =
if (requestCert && rejectUnauthorized)
Option(tlsSock.ssl.verifyError())
.map(e => new JavaScriptSSLException(js.JavaScriptException(e)))
.toLeft(())
else Either.unit
dispatcher.unsafeRunAndForget(verifyError.complete(result))
}
)
tlsSock
}
)
.evalTap(_ => verifyError.get.rethrow)
}
}
}
.adaptError { case IOException(ex) => ex }
Expand Down
49 changes: 48 additions & 1 deletion io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala
Expand Up @@ -106,7 +106,7 @@ class TLSSocketSuite extends TLSSuite {
val msg = Chunk.array(("Hello, world! " * 20000).getBytes)

val setup = for {
tlsContext <- Resource.eval(testTlsContext)
tlsContext <- Resource.eval(testTlsContext(true))
addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1"))
(serverAddress, server) = addressAndConnections
client <- Network[IO]
Expand Down Expand Up @@ -180,5 +180,52 @@ class TLSSocketSuite extends TLSSuite {
.intercept[SSLException]
}

test("mTLS client verification") { // GHSA-2cpx-6pqp-wf35
val msg = Chunk.array(("Hello, world! " * 20000).getBytes)

val setup = for {
serverContext <- Resource.eval(testTlsContext(true))
clientContext <- Resource.eval(testTlsContext(false))
addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1"))
(serverAddress, server) = addressAndConnections
client <- Network[IO]
.client(serverAddress)
.flatMap(
clientContext
.clientBuilder(_)
.withParameters(
TLSParameters(checkServerIdentity =
Some((sn, _) => Either.cond(sn == "localhost", (), new RuntimeException()))
)
)
.build
)
} yield server.flatMap(s =>
Stream.resource(
serverContext
.serverBuilder(s)
.withParameters(TLSParameters(requestCert = true.some)) // mTLS
.build
)
) -> client

Stream
.resource(setup)
.flatMap { case (server, clientSocket) =>
val echoServer = server.map { socket =>
socket.reads.chunks.foreach(socket.write(_))
}.parJoinUnbounded

val client =
Stream.exec(clientSocket.write(msg)) ++
clientSocket.reads.take(msg.size.toLong)

client.concurrently(echoServer)
}
.compile
.to(Chunk)
.intercept[SSLException]
}

}
}
7 changes: 5 additions & 2 deletions io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala
Expand Up @@ -32,7 +32,8 @@ import fs2.io.file.Path
import scala.scalajs.js

abstract class TLSSuite extends Fs2Suite {
def testTlsContext: IO[TLSContext[IO]] = Files[IO]

def testTlsContext(privateKey: Boolean): IO[TLSContext[IO]] = Files[IO]
.readAll(Path("io/shared/src/test/resources/keystore.json"))
.through(text.utf8.decode)
.compile
Expand All @@ -43,7 +44,9 @@ abstract class TLSSuite extends Fs2Suite {
SecureContext(
ca = List(certKey.cert.asRight).some,
cert = List(certKey.cert.asRight).some,
key = List(SecureContext.Key(certKey.key.asRight, "password".some)).some
key =
if (privateKey) List(SecureContext.Key(certKey.key.asRight, "password".some)).some
else None
)
)
}
Expand Down

0 comments on commit 19ce392

Please sign in to comment.