This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-grpc.git


The following commit(s) were added to refs/heads/main by this push:
     new c7a96f7f Add mTLS examples ported from akka-grpc PR #1781 (#691)
c7a96f7f is described below

commit c7a96f7f4e44add11e082d3e8351d7c077f778ef
Author: PJ Fanning <[email protected]>
AuthorDate: Fri May 8 10:02:52 2026 +0100

    Add mTLS examples ported from akka-grpc PR #1781 (#691)
    
    * Add mTLS support from akka-grpc PR #1781 (Java/Scala examples, certs, 
docs)
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko-grpc/sessions/8fc8ebec-eb62-4f02-bba7-5e964c412085
    
    Co-authored-by: pjfanning <[email protected]>
    
    * Fix code review issues: wrong class ref, println typo, README 
filename/spelling corrections
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko-grpc/sessions/8fc8ebec-eb62-4f02-bba7-5e964c412085
    
    Co-authored-by: pjfanning <[email protected]>
    
    * license headers
    
    * scalafmt
    
    * build changes
    
    * unnecessary deps
    
    * Update build.gradle
    
    ---------
    
    Co-authored-by: copilot-swe-agent[bot] 
<[email protected]>
    Co-authored-by: pjfanning <[email protected]>
---
 build.sbt                                          |   3 +
 docs/src/main/paradox/index.md                     |   1 +
 docs/src/main/paradox/mtls.md                      |  43 ++++++
 plugin-tester-java/build.gradle                    |  10 +-
 plugin-tester-java/pom.xml                         |   8 +-
 .../myapp/helloworld/MtlsGreeterClient.java        | 118 ++++++++++++++++
 .../myapp/helloworld/MtlsGreeterServer.java        | 154 +++++++++++++++++++++
 .../src/main/resources/application.conf            |   1 +
 .../src/main/resources/certs/README.md             |  63 +++++++++
 .../src/main/resources/certs/bad-client.crt        |  18 +++
 .../src/main/resources/certs/bad-client.key        |  28 ++++
 .../src/main/resources/certs/client1.crt           |  21 +++
 .../src/main/resources/certs/client1.key           |  28 ++++
 .../src/main/resources/certs/domain.ext            |   5 +
 .../src/main/resources/certs/localhost-server.crt  |  20 +++
 .../src/main/resources/certs/localhost-server.key  |  28 ++++
 .../src/main/resources/certs/rootCA.crt            |  18 +++
 .../src/main/resources/certs/rootCA.key            |  30 ++++
 plugin-tester-scala/build.gradle                   |  12 +-
 plugin-tester-scala/pom.xml                        |  13 +-
 .../src/main/resources/certs/README.md             |  63 +++++++++
 .../src/main/resources/certs/bad-client.crt        |  18 +++
 .../src/main/resources/certs/bad-client.key        |  28 ++++
 .../src/main/resources/certs/client1.crt           |  21 +++
 .../src/main/resources/certs/client1.key           |  28 ++++
 .../src/main/resources/certs/domain.ext            |   5 +
 .../src/main/resources/certs/localhost-server.crt  |  20 +++
 .../src/main/resources/certs/localhost-server.key  |  28 ++++
 .../src/main/resources/certs/rootCA.crt            |  18 +++
 .../src/main/resources/certs/rootCA.key            |  30 ++++
 plugin-tester-scala/src/main/resources/logback.xml |  21 +++
 .../myapp/helloworld/MtlsGreeterClient.scala       |  96 +++++++++++++
 .../myapp/helloworld/MtlsGreeterServer.scala       | 128 +++++++++++++++++
 .../myapp/helloworld/MtlsIntegrationSpec.scala     |  49 +++++++
 project/Dependencies.scala                         |   1 +
 35 files changed, 1166 insertions(+), 10 deletions(-)

diff --git a/build.sbt b/build.sbt
index 647fb427..df92c294 100644
--- a/build.sbt
+++ b/build.sbt
@@ -299,6 +299,8 @@ lazy val pluginTesterScala = Project(id = 
"plugin-tester-scala", base = file("pl
   .disablePlugins(MimaPlugin)
   .addPekkoModuleDependency("pekko-http-cors", "", PekkoHttpDependency.default)
   .addPekkoModuleDependency("pekko-http", "", PekkoHttpDependency.default)
+  .addPekkoModuleDependency("pekko-pki", "", PekkoCoreDependency.default)
+  .addPekkoModuleDependency("pekko-actor-testkit-typed", "test", 
PekkoCoreDependency.default)
   .settings(Dependencies.pluginTester)
   .settings(
     name := s"$pekkoPrefix-plugin-tester-scala",
@@ -312,6 +314,7 @@ lazy val pluginTesterScala = Project(id = 
"plugin-tester-scala", base = file("pl
 
 lazy val pluginTesterJava = Project(id = "plugin-tester-java", base = 
file("plugin-tester-java"))
   .disablePlugins(MimaPlugin)
+  .addPekkoModuleDependency("pekko-pki", "", PekkoCoreDependency.default)
   .settings(Dependencies.pluginTester)
   .settings(
     name := s"$pekkoPrefix-plugin-tester-java",
diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md
index b067d34b..de6d9841 100644
--- a/docs/src/main/paradox/index.md
+++ b/docs/src/main/paradox/index.md
@@ -14,6 +14,7 @@
  * [Binary Compatibility](binary-compatibility.md)
  * [gRPC API Design](apidesign.md)
  * [Deployment](deploy.md)
+ * [mTLS](mtls.md)
  * [Troubleshooting](troubleshooting.md)
  * [Release Notes](release-notes/index.md)
  * [License Report](license-report.md)
diff --git a/docs/src/main/paradox/mtls.md b/docs/src/main/paradox/mtls.md
new file mode 100644
index 00000000..6df5897a
--- /dev/null
+++ b/docs/src/main/paradox/mtls.md
@@ -0,0 +1,43 @@
+# Mutual authentication with TLS
+
+Mutual or mTLS means that just like how a client will only connect to servers 
with valid certificates, the server will
+also verify the client certificate and only allow connections if the client 
key pair is accepted by the server. This is
+useful for example in microservices where only other known services are 
allowed to interact with a service, and public access 
+should be denied.
+
+For mTLS to work the server must be set up with a keystore containing the CA 
(certificate authority) public key used to sign the individual certs
+for clients that are allowed to access the server, just like how in a regular 
TLS/HTTPS scenario the client must be able to
+verify the server certificate. 
+
+Since the CA is what controls what clients can access a service, it is likely 
an organisation or service specific CA rather
+than a normal public one like what you use for a public web server.
+
+## Setting the server up
+
+A JKS store can be prepared with the right contents, or created on the fly 
from cert files in some location the server can access for reading, 
+in this sample we use cert files available on the classpath. The server is set 
up with its own private key and cert as well as a trust 
+store with a CA to trust client certificates from:
+
+Scala
+:  @@snip 
[MtlsGreeterServer.scala](/plugin-tester-scala/src/main/scala/example/myapp/helloworld/MtlsGreeterServer.scala)
 { #full-server }
+
+Java
+:  @@snip 
[MtlsGreeterServer.java](/plugin-tester-java/src/main/java/example/myapp/helloworld/MtlsGreeterServer.java)
 { #full-server }
+
+When run the server will only accept client connections that use a keypair 
that it considers valid, other connections will be denied
+and fail with a TLS protocol error.
+
+
+## Setting the client up
+
+In the client, the trust store must be set up to trust the server cert, in our 
sample it is signed with the same CA as the
+server. The key store contains the public and private key for the client:
+
+Scala
+:  @@snip 
[MtlsGreeterClient.scala](/plugin-tester-scala/src/main/scala/example/myapp/helloworld/MtlsGreeterClient.scala)
 { #full-client }
+
+Java
+:  @@snip 
[MtlsGreeterClient.java](/plugin-tester-java/src/main/java/example/myapp/helloworld/MtlsGreeterClient.java)
 { #full-client }
+
+A client presenting a keypair will be able to connect to both servers 
requiring regular HTTPS gRPC services and mTLS servers that
+accept the client certificate.
diff --git a/plugin-tester-java/build.gradle b/plugin-tester-java/build.gradle
index 8f5cee7e..1561aaa0 100644
--- a/plugin-tester-java/build.gradle
+++ b/plugin-tester-java/build.gradle
@@ -27,13 +27,15 @@ repositories {
 def scalaFullVersion = "2.13.18"
 def scalaVersion = org.gradle.util.VersionNumber.parse(scalaFullVersion)
 def scalaBinaryVersion = "${scalaVersion.major}.${scalaVersion.minor}"
+def pekkoVersion = "2.0.0-M1"
+def pekkoHttpVersion = "2.0.0-M1"
 
 dependencies {
-  implementation "org.apache.pekko:pekko-http-cors_${scalaBinaryVersion}:1.3.0"
+  implementation 
"org.apache.pekko:pekko-http-cors_${scalaBinaryVersion}:${pekkoHttpVersion}"
+  implementation 
"org.apache.pekko:pekko-pki_${scalaBinaryVersion}:${pekkoVersion}"
   implementation "org.scala-lang:scala-library:${scalaFullVersion}"
-  testImplementation 
"org.apache.pekko:pekko-stream-testkit_${scalaBinaryVersion}:2.0.0-M1"
-  testImplementation "org.scalatest:scalatest_${scalaBinaryVersion}:3.2.19"
-  testImplementation 
"org.scalatestplus:junit-4-13_${scalaBinaryVersion}:3.2.19.0"
+  testImplementation "org.scalatest:scalatest_${scalaBinaryVersion}:3.2.20"
+  testImplementation 
"org.scalatestplus:junit-4-13_${scalaBinaryVersion}:3.2.20.0"
 }
 
 tasks.withType(Copy).configureEach {
diff --git a/plugin-tester-java/pom.xml b/plugin-tester-java/pom.xml
index ce992ccc..6980f7d3 100644
--- a/plugin-tester-java/pom.xml
+++ b/plugin-tester-java/pom.xml
@@ -24,7 +24,8 @@
     <maven.compiler.target>17</maven.compiler.target>
     <maven-dependency-plugin.version>3.8.1</maven-dependency-plugin.version>
     <maven-exec-plugin.version>3.5.1</maven-exec-plugin.version>
-    <pekko.http.version>1.3.0</pekko.http.version>
+    <pekko.version>2.0.0-M1</pekko.version>
+    <pekko.http.version>2.0.0-M1</pekko.http.version>
     <grpc.version>1.81.0</grpc.version> <!-- checked synced by 
VersionSyncCheckPlugin -->
     <project.encoding>UTF-8</project.encoding>
   </properties>
@@ -59,6 +60,11 @@
       <artifactId>pekko-http-cors_2.13</artifactId>
       <version>${pekko.http.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.pekko</groupId>
+      <artifactId>pekko-pki_2.13</artifactId>
+      <version>${pekko.version}</version>
+    </dependency>
 
     <!-- Needed for the generated client -->
     <dependency>
diff --git 
a/plugin-tester-java/src/main/java/example/myapp/helloworld/MtlsGreeterClient.java
 
b/plugin-tester-java/src/main/java/example/myapp/helloworld/MtlsGreeterClient.java
new file mode 100644
index 00000000..3f7ade82
--- /dev/null
+++ 
b/plugin-tester-java/src/main/java/example/myapp/helloworld/MtlsGreeterClient.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2018-2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+// #full-client
+package example.myapp.helloworld;
+
+import org.apache.pekko.actor.ActorSystem;
+import org.apache.pekko.grpc.GrpcClientSettings;
+import org.apache.pekko.pki.pem.DERPrivateKeyLoader;
+import org.apache.pekko.pki.pem.PEMDecoder;
+import example.myapp.helloworld.grpc.GreeterServiceClient;
+import example.myapp.helloworld.grpc.HelloReply;
+import example.myapp.helloworld.grpc.HelloRequest;
+
+import javax.net.ssl.*;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.concurrent.CompletionStage;
+import java.util.stream.Collectors;
+
+public class MtlsGreeterClient {
+
+  public static void main(String[] args) {
+    ActorSystem system = ActorSystem.create("MtlsHelloWorldClient");
+
+    GrpcClientSettings clientSettings =
+        GrpcClientSettings.connectToServiceAt("localhost", 8443, system)
+            .withSslContext(sslContext());
+
+    GreeterServiceClient client = GreeterServiceClient.create(clientSettings, 
system);
+
+    CompletionStage<HelloReply> reply =
+        client.sayHello(HelloRequest.newBuilder().setName("Jonas").build());
+
+    reply.whenComplete(
+        (response, error) -> {
+          if (error == null) {
+            System.out.println("Successful reply: " + response);
+          } else {
+            System.out.println("Request failed");
+            error.printStackTrace();
+          }
+          system.terminate();
+        });
+  }
+
+  private static SSLContext sslContext() {
+    try {
+      PrivateKey clientPrivateKey =
+          
DERPrivateKeyLoader.load(PEMDecoder.decode(classPathFileAsString("/certs/client1.key")));
+      CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+
+      // keyStore is for the client cert and private key
+      KeyStore keyStore = KeyStore.getInstance("PKCS12");
+      keyStore.load(null);
+      Certificate clientCertificate =
+          certFactory.generateCertificate(
+              
MtlsGreeterClient.class.getResourceAsStream("/certs/client1.crt"));
+      keyStore.setKeyEntry(
+          "private",
+          clientPrivateKey,
+          // No password for our private client key
+          new char[0],
+          new Certificate[] {clientCertificate});
+      KeyManagerFactory keyManagerFactory = 
KeyManagerFactory.getInstance("SunX509");
+      keyManagerFactory.init(keyStore, null);
+      KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
+
+      // trustStore is for what server certs the client trust
+      KeyStore trustStore = KeyStore.getInstance("PKCS12");
+      trustStore.load(null);
+      // accept any server cert signed by this CA
+      trustStore.setEntry(
+          "rootCA",
+          new KeyStore.TrustedCertificateEntry(
+              certFactory.generateCertificate(
+                  
MtlsGreeterClient.class.getResourceAsStream("/certs/rootCA.crt"))),
+          null);
+      TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+      tmf.init(trustStore);
+      TrustManager[] trustManagers = tmf.getTrustManagers();
+
+      SSLContext context = SSLContext.getInstance("TLS");
+      context.init(keyManagers, trustManagers, new SecureRandom());
+      return context;
+    } catch (Exception ex) {
+      throw new RuntimeException("Failed to set up SSL context for the 
client", ex);
+    }
+  }
+
+  private static String classPathFileAsString(String path) {
+    try (InputStream inputStream = 
MtlsGreeterClient.class.getResourceAsStream(path)) {
+      if (inputStream == null)
+        throw new IllegalArgumentException("'" + path + "' is not present on 
the classpath");
+      return new BufferedReader(new InputStreamReader(inputStream, 
StandardCharsets.UTF_8))
+          .lines()
+          .collect(Collectors.joining("\n"));
+    } catch (Exception ex) {
+      throw new RuntimeException("Failed reading server key from classpath", 
ex);
+    }
+  }
+}
+// #full-client
diff --git 
a/plugin-tester-java/src/main/java/example/myapp/helloworld/MtlsGreeterServer.java
 
b/plugin-tester-java/src/main/java/example/myapp/helloworld/MtlsGreeterServer.java
new file mode 100644
index 00000000..ebf44050
--- /dev/null
+++ 
b/plugin-tester-java/src/main/java/example/myapp/helloworld/MtlsGreeterServer.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2018-2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+// #full-server
+package example.myapp.helloworld;
+
+import org.apache.pekko.actor.ActorSystem;
+import org.apache.pekko.http.javadsl.ConnectionContext;
+import org.apache.pekko.http.javadsl.Http;
+import org.apache.pekko.http.javadsl.HttpsConnectionContext;
+import org.apache.pekko.http.javadsl.ServerBinding;
+import org.apache.pekko.http.javadsl.model.HttpRequest;
+import org.apache.pekko.http.javadsl.model.HttpResponse;
+import org.apache.pekko.japi.function.Function;
+import org.apache.pekko.pki.pem.DERPrivateKeyLoader;
+import org.apache.pekko.pki.pem.PEMDecoder;
+import org.apache.pekko.stream.Materializer;
+import org.apache.pekko.stream.SystemMaterializer;
+import example.myapp.helloworld.grpc.GreeterService;
+import example.myapp.helloworld.grpc.GreeterServiceHandlerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.net.ssl.*;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.concurrent.CompletionStage;
+import java.util.stream.Collectors;
+
+class MtlsGreeterServer {
+
+  private static final Logger log = 
LoggerFactory.getLogger(MtlsGreeterServer.class);
+
+  public static void main(String[] args) throws Exception {
+    ActorSystem sys = ActorSystem.create("MtlsHelloWorldServer");
+
+    run(sys)
+        .thenAccept(
+            binding -> {
+              log.info("gRPC server bound to {}", binding.localAddress());
+            });
+
+    // ActorSystem threads will keep the app alive until `system.terminate()` 
is called
+  }
+
+  public static CompletionStage<ServerBinding> run(ActorSystem sys) throws 
Exception {
+    Materializer mat = SystemMaterializer.get(sys).materializer();
+
+    // Instantiate implementation
+    GreeterService impl = new GreeterServiceImpl(mat);
+
+    Function<HttpRequest, CompletionStage<HttpResponse>> service =
+        GreeterServiceHandlerFactory.create(impl, sys);
+
+    return Http.get(sys)
+        .newServerAt("127.0.0.1", 8443)
+        .enableHttps(serverHttpContext())
+        .bind(service);
+  }
+
+  private static HttpsConnectionContext serverHttpContext() {
+    try {
+      CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+
+      // keyStore is for the server cert and private key
+      KeyStore keyStore = KeyStore.getInstance("PKCS12");
+      keyStore.load(null);
+      PrivateKey serverPrivateKey =
+          DERPrivateKeyLoader.load(
+              
PEMDecoder.decode(classPathFileAsString("/certs/localhost-server.key")));
+      Certificate serverCert =
+          certFactory.generateCertificate(
+              
MtlsGreeterServer.class.getResourceAsStream("/certs/localhost-server.crt"));
+      keyStore.setKeyEntry(
+          "private",
+          serverPrivateKey,
+          // No password for our private key
+          new char[0],
+          new Certificate[] {serverCert});
+      KeyManagerFactory keyManagerFactory = 
KeyManagerFactory.getInstance("SunX509");
+      keyManagerFactory.init(keyStore, null);
+      final KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
+
+      // trustStore is for what client certs the server trust
+      KeyStore trustStore = KeyStore.getInstance("PKCS12");
+      trustStore.load(null);
+      // any client cert signed by this CA is allowed to connect
+      trustStore.setEntry(
+          "rootCA",
+          new KeyStore.TrustedCertificateEntry(
+              certFactory.generateCertificate(
+                  
MtlsGreeterServer.class.getResourceAsStream("/certs/rootCA.crt"))),
+          null);
+      /*
+      // or specific client certs (less likely to be useful)
+      trustStore.setEntry(
+        "client1",
+        new KeyStore.TrustedCertificateEntry(
+          
certFactory.generateCertificate(getClass().getResourceAsStream("/certs/client1.crt"))),
+        null)
+       */
+      TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+      tmf.init(trustStore);
+      final TrustManager[] trustManagers = tmf.getTrustManagers();
+
+      HttpsConnectionContext httpsContext =
+          ConnectionContext.httpsServer(
+              () -> {
+                SSLContext context = SSLContext.getInstance("TLS");
+                context.init(keyManagers, trustManagers, new SecureRandom());
+
+                SSLEngine engine = context.createSSLEngine();
+                engine.setUseClientMode(false);
+
+                // require client certs
+                engine.setNeedClientAuth(true);
+
+                return engine;
+              });
+      return httpsContext;
+
+    } catch (Exception ex) {
+      throw new RuntimeException("Failed setting up the server HTTPS context", 
ex);
+    }
+  }
+
+  private static String classPathFileAsString(String path) {
+    try (InputStream inputStream = 
MtlsGreeterServer.class.getResourceAsStream(path)) {
+      if (inputStream == null)
+        throw new IllegalArgumentException("'" + path + "' is not present on 
the classpath");
+      return new BufferedReader(new InputStreamReader(inputStream, 
StandardCharsets.UTF_8))
+          .lines()
+          .collect(Collectors.joining("\n"));
+    } catch (Exception ex) {
+      throw new RuntimeException("Failed reading server key from classpath", 
ex);
+    }
+  }
+}
+// #full-server
diff --git a/plugin-tester-java/src/main/resources/application.conf 
b/plugin-tester-java/src/main/resources/application.conf
index cdc66e6b..c331165b 100644
--- a/plugin-tester-java/src/main/resources/application.conf
+++ b/plugin-tester-java/src/main/resources/application.conf
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 pekko.loglevel = INFO
+pekko.http.server.enable-http2 = on
 pekko.grpc.client {
   "helloworld.GreeterService" {
     host = 127.0.0.1
diff --git a/plugin-tester-java/src/main/resources/certs/README.md 
b/plugin-tester-java/src/main/resources/certs/README.md
new file mode 100644
index 00000000..b0f7f96c
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/README.md
@@ -0,0 +1,63 @@
+# Some notes about the certs in this directory
+
+Self-signing sample CA in rootCA.*, CA cert secret: "secret"
+Server cert for localhost signed by rootCA in localhost-server.*, no password 
for private key
+Client cert for a client to connect in client1.*, no password for private key
+
+Certs used by `MtlsGreeterServer`.
+
+## Hitting the server from curl:
+
+Hit it with no extras, will fail because server cert is self signed:
+`curl -v https://localhost:8443/Test`
+
+Accept root CA for server cert, server denies access because no client cert:
+`curl -v --cacert ./rootCA.crt https://localhost:8443/Test`
+
+Accept root CA for server cert, pass a client cert the server doesn't know, 
TLS error
+(exact error differs depending on underlying TLS impl):
+`curl -v --key client1.key --cert bad-client.crt --cacert ./rootCA.crt 
https://localhost:8443/Test`
+
+Accept root CA for server cert, pass a cert the server accepts, 404 because no 
such route:
+ `curl -v --key client1.key --cert client1.crt --cacert ./rootCA.crt 
https://localhost:8443/Test`
+
+## Re-creating the certs
+
+Creating our own CA, specifying 'secret' as password and non-important values 
for the other properties when prompted,:
+
+```shell
+openssl req -x509 -sha256 -days 36500 -newkey rsa:2048 -keyout rootCA.key -out 
rootCA.crt
+```
+
+Server key and key signing request, `localhost` for Common Name when prompted, 
empty challenge password:
+
+```shell
+openssl req -newkey rsa:2048 -nodes -keyout localhost-server.key -out 
localhost-server.csr
+```
+
+Sign server cert with our own CA (note that `domain.ext` is a manually created 
text file), use CA password `secret` from above:
+
+```shell
+openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in localhost-server.csr 
-out localhost-server.crt -days 36500 -CAcreateserial -extfile domain.ext
+```
+
+We now have localhost-server.crt and localhost-server.key for the server, and 
rootCA.crt for verifying that keypair.
+
+Same for client, no password, set a common name, but value isn't really 
important:
+
+```shell
+openssl req -newkey rsa:2048 -nodes -keyout client1.key -out client1.csr
+```
+
+Sign it:
+```shell
+openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in client1.csr -out 
client1.crt -days 36500 -CAcreateserial
+```
+
+We now have client1.crt and client1.key for the client.
+
+Additional non CA-signed certs for testing key pair that the server does not 
agree to:
+
+```shell
+openssl req -x509 -nodes -days 36500 -newkey rsa:2048 -keyout bad-client.key 
-out bad-client.crt
+```
diff --git a/plugin-tester-java/src/main/resources/certs/bad-client.crt 
b/plugin-tester-java/src/main/resources/certs/bad-client.crt
new file mode 100644
index 00000000..4e24f0bc
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/bad-client.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC8jCCAdoCCQCzfZCOC+ZRdDANBgkqhkiG9w0BAQsFADA7MQswCQYDVQQGEwJT
+RTEYMBYGA1UECgwPQmFkIEV4YW1wbGUgSW5jMRIwEAYDVQQDDAlsb2NhbGhvc3Qw
+HhcNMjMwNTAyMTkyOTE2WhcNMzMwNDI5MTkyOTE2WjA7MQswCQYDVQQGEwJTRTEY
+MBYGA1UECgwPQmFkIEV4YW1wbGUgSW5jMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqeuY0gtxL4r+xpuF/ZQAaoTN1
+kUqUisnuzY/CAZiODjmplGffXyMwhVwZb12cOTa2+3DdhxFHxTMPIVA9wYLSGoOu
+eC3R6l5NK0cBUOEovhNy1J8NzOD2uWCa2Z+8Sr6XY2IkKac8Nch49E52qt/y/veu
+emwfov9eT1dezCV78bV7QBRiY3dL6H8LsDXC8brpTyCsAMPVjaJqptTcekqVF7KK
+a8FoWKBdzzzjNHAkKeZDd6mEBu8dIGu9eNzP6vm7ENfUfi0rijCZGeiZO9k7L4yI
+V05ES1Z5QMOcfHGbstJMaF3jXqdvXcMf1QdqGcmRbRcyIRG9SjH4C1s7r6KZAgMB
+AAEwDQYJKoZIhvcNAQELBQADggEBAIxAPH+Hi6kQYIgD1xHwUAGkZ0HXceKG1PFW
+qKAiufl7Y0AzOsZN7xZkqCElwgE0cKZItxiZ8KWDyflLAa1ixRWWXdWcb7jvKMdR
+fl2kfU/iSixy/RCpMixl/m60IGpuxKLvsTlzgpWcOz3bMFWXgcOQHEn7/ICK7uZ6
+PQJsFsOg6jDrh9Fn1KUjS9Epik13PEBW2cnYFjwy9X8GIjfIL/qOprijWHg4v/0X
+3cq/ppuXZzUUQG2NsAGKtdqs/BP0fYmPqJxj9jVLqUsNlluZv6ETEYXmEz/ZwcCh
+WpjsJmH/KGT5TDxCOdJT9HWKvA3E1rFRb3sOuSke+YFlMxlyzWk=
+-----END CERTIFICATE-----
diff --git a/plugin-tester-java/src/main/resources/certs/bad-client.key 
b/plugin-tester-java/src/main/resources/certs/bad-client.key
new file mode 100644
index 00000000..f019658d
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/bad-client.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqeuY0gtxL4r+x
+puF/ZQAaoTN1kUqUisnuzY/CAZiODjmplGffXyMwhVwZb12cOTa2+3DdhxFHxTMP
+IVA9wYLSGoOueC3R6l5NK0cBUOEovhNy1J8NzOD2uWCa2Z+8Sr6XY2IkKac8Nch4
+9E52qt/y/veuemwfov9eT1dezCV78bV7QBRiY3dL6H8LsDXC8brpTyCsAMPVjaJq
+ptTcekqVF7KKa8FoWKBdzzzjNHAkKeZDd6mEBu8dIGu9eNzP6vm7ENfUfi0rijCZ
+GeiZO9k7L4yIV05ES1Z5QMOcfHGbstJMaF3jXqdvXcMf1QdqGcmRbRcyIRG9SjH4
+C1s7r6KZAgMBAAECggEAHWelXVlU9iHePp4yNu8M3YsAfT7aRlTKD86VBTmRPq9l
+csKOSBD42N2nzRtQYincLiOgjBVH/cEd1XZBiOVf0y2PmQBRputt6JGWZbu1mnlu
+kVfrN04nX2cKKqtuyeN6jFIwE1y7477DHVnGTuGaTyd7QTUMgUh0E6hLwaYksQPv
+4Ce7RuvFzvq51tKaTqKUhyrSv4Ev+U+pMsWmwEGM3F99mwwC5WZfklLBOJe7hQbW
+IVEg2K9c5kmvP8L+GHjXXdE2ZfHWBb+vcd9QFDTDiPw7JdDzucEhm3T8kL0fMoFd
+xE4Gpl9d/VJweqFhQnSqP9UEIJY/E4elADz5cf8toQKBgQDR2efrQL759h1Ro2WH
+i8uXVs0UTKawTWjxDOcMts9eiYsZVghIFUQmPqxnu0QiM7BE8gBiOQpfoOKC4V3v
+PpNK3TYlAWq11Xy/K95s6zoY5oSbjxuYdNv4J86sxu51NojgH4PH3XzwFpQIGMPN
+PijPBqp2Xkdu8HOJpZ064f+euwKBgQDP+H+UPHpZlNSUdfXdeVkomwm0czEkgXK4
+66KUBCS8n9U0afrNLS+KaV1z+HME/9JVlWxb5+ETnW3LqGtrCx219nsfussWasIj
+ThkyZ8hTIiARW4SwUFJN6JPJhilvcxn/F22rHu3BkU4JuU0bpbzf/Rkj3wd7FSf+
+M5XyWvEQuwKBgQDDd7q7+fopsOMMaSuoP0HrfPHXp7JYZDKM3ZzVze6Iu4tylR1v
+r0dkbFqA4QEM7qKRBe3vj/wmqSB0EuJBeEMQp87IV3KDXxEsrPso706VZRs+HuXw
+c2F12/Z6H258hcinIxPH9npq1E0c4Zx4sB6pACeFzDmzj4u/OiGjeGF3AwKBgAXv
+I0TJjPwtYPtzejZ9leuwsNAzUT2na+yW3Ka4j4vKS70ZIQzlsyuR4hbDChUkb439
+m3/r1+JFZbKf9aCySoC8rbq0C8Nx/GQhgFqN14t3t86G+/xD7nVGo6DmcRw6/ozm
+0DxHv6T8Tmu8m9SkIAWMJUF+xanfaqq4MhkeOy6tAoGAcbp/pCP2d8XzQNyUjOFD
+7traxW169prAwxJDJnpP6cLzueZo38PgW4t+HFUspEpG5QO0BY6r6Cwo4fm4NYM8
+fC9q0/P9xTIBffSFC0kRyVeg6dZvWYJ8+U3JsfxHNImTugoDYccWs/ja2EQFNrLx
+wYN+joYjY2A5g6qd5QMjFd8=
+-----END PRIVATE KEY-----
diff --git a/plugin-tester-java/src/main/resources/certs/client1.crt 
b/plugin-tester-java/src/main/resources/certs/client1.crt
new file mode 100644
index 00000000..79201d14
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/client1.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDbTCCAlWgAwIBAgIJAJxuw7Wkhgc6MA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNV
+BAYTAlNFMRQwEgYDVQQHDAtFeGFtcGxldG93bjEUMBIGA1UECgwLRXhhbXBsZSBJ
+bmMwIBcNMjMwNTAyMTQwMDM1WhgPMjEyMzA0MDgxNDAwMzVaMDcxCzAJBgNVBAYT
+AlNFMRQwEgYDVQQKDAtFeGFtcGxlIEluYzESMBAGA1UEAwwJbG9jYWxob3N0MIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvdF3zX/H4RAEac4Al/unCNXP
+scl1AQZv3baYBcuXH0nbh589uoDfwkAE55j+pzA39AkPLsae21Qi7vR3pTAC+N0Z
+tg3Xf9bpmKoryLlCvUB1igyXQublhcIjNfIVPFuzniUd6I+dh5zkvftx4JybWcz5
+l9Zh2PkLoiVHlvgrvfKlZcJkMQzGK3jEb8qH51SPbUwBcjLZbh6t25eqrTZIYlf2
+MXONNwkoft0GBzNxdNX6M0ePpSurUOAiz32ZKumikjC2kXJygz3AiyNzjJkSUTcc
+gdclkYxK97LdWctohoJNpqv93RJ0G5tyVUMBHr0lzvcsyN3o+VbK1m6l/lR8ywID
+AQABo3gwdjBTBgNVHSMETDBKoT2kOzA5MQswCQYDVQQGEwJTRTEUMBIGA1UEBwwL
+RXhhbXBsZXRvd24xFDASBgNVBAoMC0V4YW1wbGUgSW5jggkAybeaZAKR3I4wCQYD
+VR0TBAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEB
+AL6AzUAPIB+yO3sf7xQKFt9ApR7qYTNWkOyKSk6Nn2LzTkuWqRWP6l0w0VD6ZPg/
+c1zCgvXeUDlMSH7oh94ZY4fmaqfN3TemD3PkkSMThqFneZjV33L1FpvSJ6xhMebf
+ZbCj2WTD1yE5FwesmCDa8yL8H01yhu63iEI+eGqWps1ZLk/7klfPkZNHIkadfnuc
+HSrANVb0PtgXIuwKvrRXNTkbGU/OjeA0SdCw2uN1IBCu5nzepHuYgCrKTgIQDX1v
+ZSWbXsRcBY/YyLSDsuXE2jzVju8B9hMiEB7AK3KSFg3hGLCeVfUR8q6Uh/l/TrM3
+IetkFdy+0ekHh8T8r7L/EXs=
+-----END CERTIFICATE-----
diff --git a/plugin-tester-java/src/main/resources/certs/client1.key 
b/plugin-tester-java/src/main/resources/certs/client1.key
new file mode 100644
index 00000000..28fa6a98
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/client1.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC90XfNf8fhEARp
+zgCX+6cI1c+xyXUBBm/dtpgFy5cfSduHnz26gN/CQATnmP6nMDf0CQ8uxp7bVCLu
+9HelMAL43Rm2Ddd/1umYqivIuUK9QHWKDJdC5uWFwiM18hU8W7OeJR3oj52HnOS9
++3HgnJtZzPmX1mHY+QuiJUeW+Cu98qVlwmQxDMYreMRvyofnVI9tTAFyMtluHq3b
+l6qtNkhiV/Yxc403CSh+3QYHM3F01fozR4+lK6tQ4CLPfZkq6aKSMLaRcnKDPcCL
+I3OMmRJRNxyB1yWRjEr3st1Zy2iGgk2mq/3dEnQbm3JVQwEevSXO9yzI3ej5VsrW
+bqX+VHzLAgMBAAECggEACibWwm3QEdBafBIhY/94end86SQ+FrTybKgkT0MJLQo1
+LHauxXe4/9mOqZg8HlLs2ydU4YqL2m0QhTkb4QDFV+vzQRJScSrcSWboeo617Asz
+fhOYT2Kr6dBtM6hjzFuXKqEPjW2I3kTc0vBMsdeOU2or+xvjFciT/7MAtilFpZ3p
+27o1WLb1TWgi1hBCwmDqIqxn6sU0ExcUBRSAQEQ0bJlhQ1DhDieMR9asVI1zYbJv
+aYGcFgJ7Z5P+6O98q7D6LWUkZHHKj8VPVzIO+DMw9H4954SBK8JdNb/yuTpG2Mu6
+cw3M0vJIxnebDxSKxFGKNp8gpM8fhspWL6zGbB65wQKBgQDy+54MI+WZOVwmCHRX
+rxhoX/5men+cMW907SfUXifpFXhW3ZvzGjXgsrY5Nzd8SEy2RaWf1g4SepVk7ZDT
+YwSn1Y0Mbt5fzEcdBX/uHxIV1PTV8E9uZq9pLj0nP7X4Pe7IgRCm9/C2PL+Tgwmn
+26KvMHAocjDe4Ne1SPnsP72vqwKBgQDH/LmzyxqcmFDAIgQbI0v24Gr/Kd5qkpEM
+qJLxyczzzUofYuC0Z202wFKcXxUpKqaLurkep49vDf25Nowr4CtAiNTod5Ty+ZV9
+STTtx+/uaXk+n/Y+2o3t+d9vwMHzPHueXHDEU0QXEZfClAY7nmENgUl/xz7Mv+kj
+SG/4kLXHYQJ/BbHgAmjU+MJfZoTMNUHlUIzvaXd1hjOiaRsl09RhGxVlvKN1BD2Z
+BasqmiyxIDiRk7QOLbDWo5g76CGpQ0sO0OAwbhorHBOtlwCJ/wq7Yceb9WesdOnz
+MoPi6wiTOz44Wnqr6T3mZl8GHm7zyvta1MBN4KTMgGzEoXsUYHUd/QKBgQCk35gB
+wDpKS9CW9fRIo0rnV5EemFgDqJ3ov7mVmPddMCwhwBTc5j/F2bzBqin57G2t2Nzx
+htbbib9ZyLy7F27RH33XwW6M+nLh/U6jkiged9o7ZQlQPEKypUQuD85WR9Dqd++I
+C9Wg5yIkioCw+hutVJ9RtuPxTW5ZZkjZtgQHQQKBgGUXI8B8MI1A03OQ3+EIJUBg
+Jc2IB1hALwtsLBmlGwPOI3sKv+Dq97j8hKy7yFGHZtucHik5YOFWERPL88X6pXjg
+CdaX6IIncPSZOd0vbivd5PEsSN2PHE2h9WkFEncXKR9T+UcTOjV11Q0Y9fByPw/1
+1tEcRcj5HH58Ix2ZS7/w
+-----END PRIVATE KEY-----
diff --git a/plugin-tester-java/src/main/resources/certs/domain.ext 
b/plugin-tester-java/src/main/resources/certs/domain.ext
new file mode 100644
index 00000000..45324cc7
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/domain.ext
@@ -0,0 +1,5 @@
+authorityKeyIdentifier=keyid,issuer
+basicConstraints=CA:FALSE
+subjectAltName = @alt_names
+[alt_names]
+DNS.1 = localhost
diff --git a/plugin-tester-java/src/main/resources/certs/localhost-server.crt 
b/plugin-tester-java/src/main/resources/certs/localhost-server.crt
new file mode 100644
index 00000000..0b309d25
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/localhost-server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIJAJxuw7Wkhgc5MA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNV
+BAYTAlNFMRQwEgYDVQQHDAtFeGFtcGxldG93bjEUMBIGA1UECgwLRXhhbXBsZSBJ
+bmMwIBcNMjMwNTAyMTM0NzM4WhgPMjEyMzA0MDgxMzQ3MzhaMBQxEjAQBgNVBAMM
+CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANBsMucJ
+duEpgqi8RjQfR2gr/exaH0lS8GC2fzcDDrcuUJTbuqYX09Hea0Qg/Nx5HsSyhdN0
+HI3jFum/boOIGQtskrkId6vQUa1gZEws/qkPdQEcz2sY/G41UNSLZESzGaRhYvvD
+PjydmCUdBftV5gHoDKy0vdHIP44FRIwz/xeVEc/W5QcPa8DDuQDURHZyGYeAA6ZC
+5yQicNo7J/n85xx6VMDy6V9PpB6UR0id0v2luSyCKoIhoKxfVnU5aRN6QjHfLvys
+G8kyqjTFd11TYb4B99eC6uN0IutMTSvvxqzuhM8vZ38Ck4HjqO4qBy4D9w6RxZnP
+4zTksPqlYwNaeaUCAwEAAaN4MHYwUwYDVR0jBEwwSqE9pDswOTELMAkGA1UEBhMC
+U0UxFDASBgNVBAcMC0V4YW1wbGV0b3duMRQwEgYDVQQKDAtFeGFtcGxlIEluY4IJ
+AMm3mmQCkdyOMAkGA1UdEwQCMAAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqG
+SIb3DQEBCwUAA4IBAQBENcH6yNMywa7tBtelqADzds+yryVEGPNLkGABGdwzDxSm
+6tHZXdl1saT+AjhkoEFDABWhhqSkEMbObLIFtKpadtsRwq0OO1CoM7Bv0nsv5jSO
+s4mwdF4pO+4CxZARjQmfOXISiv6peDnmFlxK/sUDJUmUsY0gy6cZF4O+1NrQn4Np
+ew860WvR6L4JT80oDzKzqkHz52RzaRq1QmcvdohVVy8C8A9YrdIbKgQUhugIsVsq
+OOqzfBFcqkeywzdzWq5VmBS0bjNNz5+wpPKsCk7eXeFRLE/awkD2GeZoLw3LDPEg
+YdvNxWzymoNlB/MiP4HdGOK1DNcHLuwyOGkcNkV7
+-----END CERTIFICATE-----
diff --git a/plugin-tester-java/src/main/resources/certs/localhost-server.key 
b/plugin-tester-java/src/main/resources/certs/localhost-server.key
new file mode 100644
index 00000000..25c1f340
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/localhost-server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDQbDLnCXbhKYKo
+vEY0H0doK/3sWh9JUvBgtn83Aw63LlCU27qmF9PR3mtEIPzceR7EsoXTdByN4xbp
+v26DiBkLbJK5CHer0FGtYGRMLP6pD3UBHM9rGPxuNVDUi2REsxmkYWL7wz48nZgl
+HQX7VeYB6AystL3RyD+OBUSMM/8XlRHP1uUHD2vAw7kA1ER2chmHgAOmQuckInDa
+Oyf5/OccelTA8ulfT6QelEdIndL9pbksgiqCIaCsX1Z1OWkTekIx3y78rBvJMqo0
+xXddU2G+AffXgurjdCLrTE0r78as7oTPL2d/ApOB46juKgcuA/cOkcWZz+M05LD6
+pWMDWnmlAgMBAAECggEBALCrBnrQivRRO2/MJ7YGzYB/yb2OpvaAV0GjcDIxZUfg
++m0z1AL2L5a18jbNv4kjIfGZYdbblViwJbv9iK/1rUUBw10U0FvTOWi9TEdF3Jdx
+grxur2MYyuCgUOPZRCT3q8SqyDygQyEedNkAwRFKvqzfBd9fVYd9NmIsFO7DJHfX
+XFUJuxz4z0IrxW5XsTQ7PLPtbcz3hZYF6GSZlCBAw6A7rKCGvjqOXmRFuz5t0tk5
+HqIBEFcMaGFQOhxsX9L5U6LseuKKj30A6OFsd1yLa5r42towaXoSY0XveRiaAYRu
+4N1+zETCCQE/HHMNYfMpNDz6AtkErcDs28TFqIlSDAECgYEA8JlqSa+8umimP6cj
+3aft1zZ9M+voCKntxkIcdJ1dS8MfrqFwTZz5pMzwKYtdCT8Dhw4ZyOA1Stxg2pPF
+cW7XIHhMSlWRdfnrEGwHPGgZ2L5k9MtHZrSYCCJCSqqZjOSbosEfpuyzDTqgA0+S
+rHMYXkoWFRAThRu3yIHK41X3PhcCgYEA3cOFTnsQuCaW81TWAeKpfWFVPJVRDQOX
+LjuAprVNJ+EGiMuDWfvKFcuTdG4FuOawY9ZMbQYRNEZLoSGSEU4bSR2OCXMr0Wyy
+fG2C1cFN2QrylrhrkrvbFajOJK4UCnQLof92yRuuuUlKlqRMDN2rtcorLJ3BpSae
+noK6JHmBN6MCgYEAo2XNRVXQOliv7zK3rOVLJYmf5g8keh3NmYN0h84Helh9v79r
+4YnmEQINaGl5ObpNzv7IjB+YkcqxDECnKq4385k/VoxeSVz9Qx3anC+mvggvz//t
+8dZcGcoKc2MA/SqUeCfoMxk1UJqr6RO1bOCNgBuYe517ZD66xbU/8LyFOOkCgYEA
+02VTiSmFGZYnpRPE4Y049j03bIYF+jrm/XpZPBFt2EsI2JPvxXJhBH/IM1/B8q1t
+je41cmQrOEKeS55dyENFfWBACsAQEBXm2vfllXAsjm6CK6znVrver3n38D1E+2X9
+xNJqYHEUEKpOAOXjXQxeZ++tUl2bv5vd7so9ORHeXLMCgYEAkngbi1raD6RDRj+3
+8wvN/fl9s37l+9yCfj7VfeqQ899ypEBbPhFZtpTmkFtratMTEE2je4YxB4av45Zt
+Obzd1nBcWNyuEEq2sSxfjUnS3ruAEFM8aLK4mOgsm9mtni6OVel9BI9qY2sCWNXr
+BABBs7f/lr//lKFjor/fYIB1UsY=
+-----END PRIVATE KEY-----
diff --git a/plugin-tester-java/src/main/resources/certs/rootCA.crt 
b/plugin-tester-java/src/main/resources/certs/rootCA.crt
new file mode 100644
index 00000000..c2e4953d
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/rootCA.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC8DCCAdgCCQDJt5pkApHcjjANBgkqhkiG9w0BAQsFADA5MQswCQYDVQQGEwJT
+RTEUMBIGA1UEBwwLRXhhbXBsZXRvd24xFDASBgNVBAoMC0V4YW1wbGUgSW5jMCAX
+DTIzMDUwMjEzNDQ1OFoYDzIxMjMwNDA4MTM0NDU4WjA5MQswCQYDVQQGEwJTRTEU
+MBIGA1UEBwwLRXhhbXBsZXRvd24xFDASBgNVBAoMC0V4YW1wbGUgSW5jMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrDswRWKKj/HGl9IFTX1Ux4+3cc+
+XMuDdnA70sixEtWPKPRvgJqRb8WZd6ED6BPnZltWppAPB8AwHm3IDdMaEtGIKup0
+NaWZf2d+lqA/5caayGfZpu1IYUFJICAq8SdlF5LOwXWznP+g7632tRukAJk5IoC9
+F5C6GxExqL38waiLUj7FrQaw0vMi9lDS30pPVK4g3msoaD60qI2SjtPL0qO8iP6Z
+FUI79J7ugZNgFYUaxDRVIKCG2pknTcD2nx+n1AX+tcyQN4ybf9aIv5TyoE5Yki6I
++k2a3sDNzouxLTKPlgt5iCqU0440PfkYhP1rJ5p8cUEmwaSLaHUaRRu8YwIDAQAB
+MA0GCSqGSIb3DQEBCwUAA4IBAQAVAOmoL+wMIoqDgTfqwLsEGA7FiE1HerDE0mEv
+LcMECVavawexrsl2G4dao+YTMaRC0761aoIrxRoh8nm/jxly+ZWYPNXRVlMRtfMx
+WVALEOHhVJH/83swQzVNbuk/kz91Jeg0VS90OAw6QeO3ELg5HKqxdjqxr1+Emsz1
+q47dK2BWIMA5ux+pzL9jf1KVrx/tX3lnZH1Fr2Pup/l3FrHeLO+N6gFUirgiTebH
+JadlTQu+wM+CNVWy2n8tZLjkWZVPdI+D/WBe8wEznMHG1l4FOqsTO3FuxYCsFB+T
+M0YwXvSI+kjljc9ZEKEmuA1xwZqIt1MEJ0K7crSlMl55j1K6
+-----END CERTIFICATE-----
diff --git a/plugin-tester-java/src/main/resources/certs/rootCA.key 
b/plugin-tester-java/src/main/resources/certs/rootCA.key
new file mode 100644
index 00000000..c8ae5daa
--- /dev/null
+++ b/plugin-tester-java/src/main/resources/certs/rootCA.key
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQI+9FLeLL2kl0CAggA
+MB0GCWCGSAFlAwQBKgQQr00F6ZDUAs2plMALi3ELbgSCBNDhYZP8LFd4VfJ4rQcc
+BDojlRvxdW5GlgGpNaaXcNDZjza2dWJuwik60reZizL6ldOUF8QCAal+J/r8gzNt
+3JbGaGmEgzTYvmusLab1JwHJwH7tJWwBNTihtTYwysr9KQ5r02uw5a4UcvbQilL0
+mC1Iz6ZJMS0YNWK64hFmcbP/niO9a8AHvNt8E1TedM/8E12SBnXNoClHs12FAebf
+AccIO8ZJVBS5l9+Zffajem9FVKrd2Zb6ymC3YJ8513pdFsx7TgxsAQv2nIMZWnh4
+j9l/cYoDONhg2VwNyYiDlzAua15OSg99Jz4LtNILD9VbRHdeBkg/gcInbjoqzMkD
+jPHJ6PUSiYgRDMwmSbNTDEnHiUUEsj2VqE8JyxHLYU1EV90ceq4YtckgdO2QUCT6
++kKnC3Fi/v/7Pk1JHCCGLTarJyYKQMhw3zOZe0nPndBOeWwf0Pajlf+iCz7SKEyr
+JG8o+anad9VzksoRpj5U+aP6OnyjjW+UfwBxztyfrcIRCpGoyEY8pLC4sOc7J2IQ
+jCLODCkQu8lM3ILqK3Ig4I6A44JYnYHQbP9pZz9rpt6Hqi/CTKWsuRtmDjYjmzj8
+ZHXogv+BlgJA20fQ5l7M3NXotdhX94A0lqDWN80LIyoDBSIgxw+pnaYT2QYIfY1b
+3O4N1cuTFdLOpNKhO2iOY/hA1fGgea4uGTkT0fWshuP9Sxf8+P+WCVdQxxGasMiS
+DWn9D861eSHZ/bq2Lbyy/6kjSOquzRDf8rO72i9Puxl16KRybiOSuRR+mNKf3zCk
+tiXSwttXSSrdhSF/sZsYHSq6sFmqw9HLKYXBAWAyIC9jqHK3zUM3xNMevDXHg78w
+pGHu7fNWICZD7mlJRxGXxArWs/YltrnOAXmlUemA+8N1cqJX2On+f41ow+XEV9Va
+lLwOBg4r03MkDxJEuwIneKzPxI26MzZTN8ppAnugpylhHiRpo577edAK6yNghyYt
+d8FyuBAprmi7zIrDXJLYmlSqmkwJemKo73fOTX6z6ravH+SY0G+24rzJXlsIHyDI
+JoY2EmbvD6bhhjKv55+s7kBG3HjC7S6Hj8BcXrZfFaje/YrNbUjHOpGV/j/APXlX
+9157K8XFH11Yh7xOzN1P3wA1+AK0+i6p273HqoH4MGk2eKxv0VWz5xpnKlwe10Ao
+uQMbLkggWVY/7L26X1v4af/YAerau2bDMwXxjs/2/O1CB9bbvL88z7DwkCNZq1JT
+lAgrbj+pkbduXO34WAUbmLajdLXiydhcyshTOYhFHka2dKnROWaoalSsKD6bqxRH
+MlhO56i2QmikLhzrNqHpyvQKaD1WG1tGM2U0IEAPQEo/jUZEeV+l1gkVHK8GaZtq
+TpzBx7q6qHnTesjYVoBxtCI/wdx5JBxS7VyCYmokSI24ATgHmMonroV3kNrjNoWA
+d7MqOALmWakQcW66NwNO+airTfHC2sHespO/tLpO5cP3cdsnV/pQwUgkfEmsc55p
+Hsqi3mFE226nttz8M63km7ch5p5kI6aNhFVkd2wLzHomlNwHCE+z9rDaXEdDcphy
++zyrHKN4qpMgq0PFYZ/V+sS3APzrdEGmD7g/IGaGO/QBwQwDchUgWYt8sKGe6TUu
+bboa0n7bo9vN8U3I66JoYP68tA==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/plugin-tester-scala/build.gradle b/plugin-tester-scala/build.gradle
index e2f49772..cf52c461 100644
--- a/plugin-tester-scala/build.gradle
+++ b/plugin-tester-scala/build.gradle
@@ -22,13 +22,17 @@ repositories {
 def scalaFullVersion = "2.13.18"
 def scalaVersion = org.gradle.util.VersionNumber.parse(scalaFullVersion)
 def scalaBinaryVersion = "${scalaVersion.major}.${scalaVersion.minor}"
+def pekkoVersion = "2.0.0-M1"
+def pekkoHttpVersion = "2.0.0-M1"
 
 dependencies {
-  implementation "org.apache.pekko:pekko-http-cors_${scalaBinaryVersion}:1.3.0"
+  implementation 
"org.apache.pekko:pekko-http-cors_${scalaBinaryVersion}:${pekkoHttpVersion}"
+  implementation 
"org.apache.pekko:pekko-pki_${scalaBinaryVersion}:${pekkoVersion}"
   implementation "org.scala-lang:scala-library:${scalaFullVersion}"
-  testImplementation 
"org.apache.pekko:pekko-stream-testkit_${scalaBinaryVersion}:2.0.0-M1"
-  testImplementation "org.scalatest:scalatest_${scalaBinaryVersion}:3.2.19"
-  testImplementation 
"org.scalatestplus:junit-4-13_${scalaBinaryVersion}:3.2.19.0"
+  testImplementation 
"org.apache.pekko:pekko-actor-testkit-typed_${scalaBinaryVersion}:${pekkoVersion}"
+  testImplementation 
"org.apache.pekko:pekko-stream-testkit_${scalaBinaryVersion}:${pekkoVersion}"
+  testImplementation "org.scalatest:scalatest_${scalaBinaryVersion}:3.2.20"
+  testImplementation 
"org.scalatestplus:junit-4-13_${scalaBinaryVersion}:3.2.20.0"
 }
 
 tasks.withType(Copy).configureEach {
diff --git a/plugin-tester-scala/pom.xml b/plugin-tester-scala/pom.xml
index 93f9a986..93a24998 100644
--- a/plugin-tester-scala/pom.xml
+++ b/plugin-tester-scala/pom.xml
@@ -23,7 +23,7 @@
     <maven.compiler.source>17</maven.compiler.source>
     <maven.compiler.target>17</maven.compiler.target>
     <pekko.version>2.0.0-M1</pekko.version>
-    <pekko.http.version>1.3.0</pekko.http.version>
+    <pekko.http.version>2.0.0-M1</pekko.http.version>
     <grpc.version>1.81.0</grpc.version> <!-- checked synced by 
VersionSyncCheckPlugin -->
     <project.encoding>UTF-8</project.encoding>
   </properties>
@@ -58,7 +58,18 @@
       <artifactId>pekko-http-cors_2.13</artifactId>
       <version>${pekko.http.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.pekko</groupId>
+      <artifactId>pekko-pki_2.13</artifactId>
+      <version>${pekko.version}</version>
+    </dependency>
 
+    <dependency>
+      <groupId>org.apache.pekko</groupId>
+      <artifactId>pekko-actor-testkit-typed_2.13</artifactId>
+      <version>${pekko.version}</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.pekko</groupId>
       <artifactId>pekko-stream-testkit_2.13</artifactId>
diff --git a/plugin-tester-scala/src/main/resources/certs/README.md 
b/plugin-tester-scala/src/main/resources/certs/README.md
new file mode 100644
index 00000000..b0f7f96c
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/README.md
@@ -0,0 +1,63 @@
+# Some notes about the certs in this directory
+
+Self-signing sample CA in rootCA.*, CA cert secret: "secret"
+Server cert for localhost signed by rootCA in localhost-server.*, no password 
for private key
+Client cert for a client to connect in client1.*, no password for private key
+
+Certs used by `MtlsGreeterServer`.
+
+## Hitting the server from curl:
+
+Hit it with no extras, will fail because server cert is self signed:
+`curl -v https://localhost:8443/Test`
+
+Accept root CA for server cert, server denies access because no client cert:
+`curl -v --cacert ./rootCA.crt https://localhost:8443/Test`
+
+Accept root CA for server cert, pass a client cert the server doesn't know, 
TLS error
+(exact error differs depending on underlying TLS impl):
+`curl -v --key client1.key --cert bad-client.crt --cacert ./rootCA.crt 
https://localhost:8443/Test`
+
+Accept root CA for server cert, pass a cert the server accepts, 404 because no 
such route:
+ `curl -v --key client1.key --cert client1.crt --cacert ./rootCA.crt 
https://localhost:8443/Test`
+
+## Re-creating the certs
+
+Creating our own CA, specifying 'secret' as password and non-important values 
for the other properties when prompted,:
+
+```shell
+openssl req -x509 -sha256 -days 36500 -newkey rsa:2048 -keyout rootCA.key -out 
rootCA.crt
+```
+
+Server key and key signing request, `localhost` for Common Name when prompted, 
empty challenge password:
+
+```shell
+openssl req -newkey rsa:2048 -nodes -keyout localhost-server.key -out 
localhost-server.csr
+```
+
+Sign server cert with our own CA (note that `domain.ext` is a manually created 
text file), use CA password `secret` from above:
+
+```shell
+openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in localhost-server.csr 
-out localhost-server.crt -days 36500 -CAcreateserial -extfile domain.ext
+```
+
+We now have localhost-server.crt and localhost-server.key for the server, and 
rootCA.crt for verifying that keypair.
+
+Same for client, no password, set a common name, but value isn't really 
important:
+
+```shell
+openssl req -newkey rsa:2048 -nodes -keyout client1.key -out client1.csr
+```
+
+Sign it:
+```shell
+openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in client1.csr -out 
client1.crt -days 36500 -CAcreateserial
+```
+
+We now have client1.crt and client1.key for the client.
+
+Additional non CA-signed certs for testing key pair that the server does not 
agree to:
+
+```shell
+openssl req -x509 -nodes -days 36500 -newkey rsa:2048 -keyout bad-client.key 
-out bad-client.crt
+```
diff --git a/plugin-tester-scala/src/main/resources/certs/bad-client.crt 
b/plugin-tester-scala/src/main/resources/certs/bad-client.crt
new file mode 100644
index 00000000..4e24f0bc
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/bad-client.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC8jCCAdoCCQCzfZCOC+ZRdDANBgkqhkiG9w0BAQsFADA7MQswCQYDVQQGEwJT
+RTEYMBYGA1UECgwPQmFkIEV4YW1wbGUgSW5jMRIwEAYDVQQDDAlsb2NhbGhvc3Qw
+HhcNMjMwNTAyMTkyOTE2WhcNMzMwNDI5MTkyOTE2WjA7MQswCQYDVQQGEwJTRTEY
+MBYGA1UECgwPQmFkIEV4YW1wbGUgSW5jMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqeuY0gtxL4r+xpuF/ZQAaoTN1
+kUqUisnuzY/CAZiODjmplGffXyMwhVwZb12cOTa2+3DdhxFHxTMPIVA9wYLSGoOu
+eC3R6l5NK0cBUOEovhNy1J8NzOD2uWCa2Z+8Sr6XY2IkKac8Nch49E52qt/y/veu
+emwfov9eT1dezCV78bV7QBRiY3dL6H8LsDXC8brpTyCsAMPVjaJqptTcekqVF7KK
+a8FoWKBdzzzjNHAkKeZDd6mEBu8dIGu9eNzP6vm7ENfUfi0rijCZGeiZO9k7L4yI
+V05ES1Z5QMOcfHGbstJMaF3jXqdvXcMf1QdqGcmRbRcyIRG9SjH4C1s7r6KZAgMB
+AAEwDQYJKoZIhvcNAQELBQADggEBAIxAPH+Hi6kQYIgD1xHwUAGkZ0HXceKG1PFW
+qKAiufl7Y0AzOsZN7xZkqCElwgE0cKZItxiZ8KWDyflLAa1ixRWWXdWcb7jvKMdR
+fl2kfU/iSixy/RCpMixl/m60IGpuxKLvsTlzgpWcOz3bMFWXgcOQHEn7/ICK7uZ6
+PQJsFsOg6jDrh9Fn1KUjS9Epik13PEBW2cnYFjwy9X8GIjfIL/qOprijWHg4v/0X
+3cq/ppuXZzUUQG2NsAGKtdqs/BP0fYmPqJxj9jVLqUsNlluZv6ETEYXmEz/ZwcCh
+WpjsJmH/KGT5TDxCOdJT9HWKvA3E1rFRb3sOuSke+YFlMxlyzWk=
+-----END CERTIFICATE-----
diff --git a/plugin-tester-scala/src/main/resources/certs/bad-client.key 
b/plugin-tester-scala/src/main/resources/certs/bad-client.key
new file mode 100644
index 00000000..f019658d
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/bad-client.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqeuY0gtxL4r+x
+puF/ZQAaoTN1kUqUisnuzY/CAZiODjmplGffXyMwhVwZb12cOTa2+3DdhxFHxTMP
+IVA9wYLSGoOueC3R6l5NK0cBUOEovhNy1J8NzOD2uWCa2Z+8Sr6XY2IkKac8Nch4
+9E52qt/y/veuemwfov9eT1dezCV78bV7QBRiY3dL6H8LsDXC8brpTyCsAMPVjaJq
+ptTcekqVF7KKa8FoWKBdzzzjNHAkKeZDd6mEBu8dIGu9eNzP6vm7ENfUfi0rijCZ
+GeiZO9k7L4yIV05ES1Z5QMOcfHGbstJMaF3jXqdvXcMf1QdqGcmRbRcyIRG9SjH4
+C1s7r6KZAgMBAAECggEAHWelXVlU9iHePp4yNu8M3YsAfT7aRlTKD86VBTmRPq9l
+csKOSBD42N2nzRtQYincLiOgjBVH/cEd1XZBiOVf0y2PmQBRputt6JGWZbu1mnlu
+kVfrN04nX2cKKqtuyeN6jFIwE1y7477DHVnGTuGaTyd7QTUMgUh0E6hLwaYksQPv
+4Ce7RuvFzvq51tKaTqKUhyrSv4Ev+U+pMsWmwEGM3F99mwwC5WZfklLBOJe7hQbW
+IVEg2K9c5kmvP8L+GHjXXdE2ZfHWBb+vcd9QFDTDiPw7JdDzucEhm3T8kL0fMoFd
+xE4Gpl9d/VJweqFhQnSqP9UEIJY/E4elADz5cf8toQKBgQDR2efrQL759h1Ro2WH
+i8uXVs0UTKawTWjxDOcMts9eiYsZVghIFUQmPqxnu0QiM7BE8gBiOQpfoOKC4V3v
+PpNK3TYlAWq11Xy/K95s6zoY5oSbjxuYdNv4J86sxu51NojgH4PH3XzwFpQIGMPN
+PijPBqp2Xkdu8HOJpZ064f+euwKBgQDP+H+UPHpZlNSUdfXdeVkomwm0czEkgXK4
+66KUBCS8n9U0afrNLS+KaV1z+HME/9JVlWxb5+ETnW3LqGtrCx219nsfussWasIj
+ThkyZ8hTIiARW4SwUFJN6JPJhilvcxn/F22rHu3BkU4JuU0bpbzf/Rkj3wd7FSf+
+M5XyWvEQuwKBgQDDd7q7+fopsOMMaSuoP0HrfPHXp7JYZDKM3ZzVze6Iu4tylR1v
+r0dkbFqA4QEM7qKRBe3vj/wmqSB0EuJBeEMQp87IV3KDXxEsrPso706VZRs+HuXw
+c2F12/Z6H258hcinIxPH9npq1E0c4Zx4sB6pACeFzDmzj4u/OiGjeGF3AwKBgAXv
+I0TJjPwtYPtzejZ9leuwsNAzUT2na+yW3Ka4j4vKS70ZIQzlsyuR4hbDChUkb439
+m3/r1+JFZbKf9aCySoC8rbq0C8Nx/GQhgFqN14t3t86G+/xD7nVGo6DmcRw6/ozm
+0DxHv6T8Tmu8m9SkIAWMJUF+xanfaqq4MhkeOy6tAoGAcbp/pCP2d8XzQNyUjOFD
+7traxW169prAwxJDJnpP6cLzueZo38PgW4t+HFUspEpG5QO0BY6r6Cwo4fm4NYM8
+fC9q0/P9xTIBffSFC0kRyVeg6dZvWYJ8+U3JsfxHNImTugoDYccWs/ja2EQFNrLx
+wYN+joYjY2A5g6qd5QMjFd8=
+-----END PRIVATE KEY-----
diff --git a/plugin-tester-scala/src/main/resources/certs/client1.crt 
b/plugin-tester-scala/src/main/resources/certs/client1.crt
new file mode 100644
index 00000000..79201d14
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/client1.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDbTCCAlWgAwIBAgIJAJxuw7Wkhgc6MA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNV
+BAYTAlNFMRQwEgYDVQQHDAtFeGFtcGxldG93bjEUMBIGA1UECgwLRXhhbXBsZSBJ
+bmMwIBcNMjMwNTAyMTQwMDM1WhgPMjEyMzA0MDgxNDAwMzVaMDcxCzAJBgNVBAYT
+AlNFMRQwEgYDVQQKDAtFeGFtcGxlIEluYzESMBAGA1UEAwwJbG9jYWxob3N0MIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvdF3zX/H4RAEac4Al/unCNXP
+scl1AQZv3baYBcuXH0nbh589uoDfwkAE55j+pzA39AkPLsae21Qi7vR3pTAC+N0Z
+tg3Xf9bpmKoryLlCvUB1igyXQublhcIjNfIVPFuzniUd6I+dh5zkvftx4JybWcz5
+l9Zh2PkLoiVHlvgrvfKlZcJkMQzGK3jEb8qH51SPbUwBcjLZbh6t25eqrTZIYlf2
+MXONNwkoft0GBzNxdNX6M0ePpSurUOAiz32ZKumikjC2kXJygz3AiyNzjJkSUTcc
+gdclkYxK97LdWctohoJNpqv93RJ0G5tyVUMBHr0lzvcsyN3o+VbK1m6l/lR8ywID
+AQABo3gwdjBTBgNVHSMETDBKoT2kOzA5MQswCQYDVQQGEwJTRTEUMBIGA1UEBwwL
+RXhhbXBsZXRvd24xFDASBgNVBAoMC0V4YW1wbGUgSW5jggkAybeaZAKR3I4wCQYD
+VR0TBAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEB
+AL6AzUAPIB+yO3sf7xQKFt9ApR7qYTNWkOyKSk6Nn2LzTkuWqRWP6l0w0VD6ZPg/
+c1zCgvXeUDlMSH7oh94ZY4fmaqfN3TemD3PkkSMThqFneZjV33L1FpvSJ6xhMebf
+ZbCj2WTD1yE5FwesmCDa8yL8H01yhu63iEI+eGqWps1ZLk/7klfPkZNHIkadfnuc
+HSrANVb0PtgXIuwKvrRXNTkbGU/OjeA0SdCw2uN1IBCu5nzepHuYgCrKTgIQDX1v
+ZSWbXsRcBY/YyLSDsuXE2jzVju8B9hMiEB7AK3KSFg3hGLCeVfUR8q6Uh/l/TrM3
+IetkFdy+0ekHh8T8r7L/EXs=
+-----END CERTIFICATE-----
diff --git a/plugin-tester-scala/src/main/resources/certs/client1.key 
b/plugin-tester-scala/src/main/resources/certs/client1.key
new file mode 100644
index 00000000..28fa6a98
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/client1.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC90XfNf8fhEARp
+zgCX+6cI1c+xyXUBBm/dtpgFy5cfSduHnz26gN/CQATnmP6nMDf0CQ8uxp7bVCLu
+9HelMAL43Rm2Ddd/1umYqivIuUK9QHWKDJdC5uWFwiM18hU8W7OeJR3oj52HnOS9
++3HgnJtZzPmX1mHY+QuiJUeW+Cu98qVlwmQxDMYreMRvyofnVI9tTAFyMtluHq3b
+l6qtNkhiV/Yxc403CSh+3QYHM3F01fozR4+lK6tQ4CLPfZkq6aKSMLaRcnKDPcCL
+I3OMmRJRNxyB1yWRjEr3st1Zy2iGgk2mq/3dEnQbm3JVQwEevSXO9yzI3ej5VsrW
+bqX+VHzLAgMBAAECggEACibWwm3QEdBafBIhY/94end86SQ+FrTybKgkT0MJLQo1
+LHauxXe4/9mOqZg8HlLs2ydU4YqL2m0QhTkb4QDFV+vzQRJScSrcSWboeo617Asz
+fhOYT2Kr6dBtM6hjzFuXKqEPjW2I3kTc0vBMsdeOU2or+xvjFciT/7MAtilFpZ3p
+27o1WLb1TWgi1hBCwmDqIqxn6sU0ExcUBRSAQEQ0bJlhQ1DhDieMR9asVI1zYbJv
+aYGcFgJ7Z5P+6O98q7D6LWUkZHHKj8VPVzIO+DMw9H4954SBK8JdNb/yuTpG2Mu6
+cw3M0vJIxnebDxSKxFGKNp8gpM8fhspWL6zGbB65wQKBgQDy+54MI+WZOVwmCHRX
+rxhoX/5men+cMW907SfUXifpFXhW3ZvzGjXgsrY5Nzd8SEy2RaWf1g4SepVk7ZDT
+YwSn1Y0Mbt5fzEcdBX/uHxIV1PTV8E9uZq9pLj0nP7X4Pe7IgRCm9/C2PL+Tgwmn
+26KvMHAocjDe4Ne1SPnsP72vqwKBgQDH/LmzyxqcmFDAIgQbI0v24Gr/Kd5qkpEM
+qJLxyczzzUofYuC0Z202wFKcXxUpKqaLurkep49vDf25Nowr4CtAiNTod5Ty+ZV9
+STTtx+/uaXk+n/Y+2o3t+d9vwMHzPHueXHDEU0QXEZfClAY7nmENgUl/xz7Mv+kj
+SG/4kLXHYQJ/BbHgAmjU+MJfZoTMNUHlUIzvaXd1hjOiaRsl09RhGxVlvKN1BD2Z
+BasqmiyxIDiRk7QOLbDWo5g76CGpQ0sO0OAwbhorHBOtlwCJ/wq7Yceb9WesdOnz
+MoPi6wiTOz44Wnqr6T3mZl8GHm7zyvta1MBN4KTMgGzEoXsUYHUd/QKBgQCk35gB
+wDpKS9CW9fRIo0rnV5EemFgDqJ3ov7mVmPddMCwhwBTc5j/F2bzBqin57G2t2Nzx
+htbbib9ZyLy7F27RH33XwW6M+nLh/U6jkiged9o7ZQlQPEKypUQuD85WR9Dqd++I
+C9Wg5yIkioCw+hutVJ9RtuPxTW5ZZkjZtgQHQQKBgGUXI8B8MI1A03OQ3+EIJUBg
+Jc2IB1hALwtsLBmlGwPOI3sKv+Dq97j8hKy7yFGHZtucHik5YOFWERPL88X6pXjg
+CdaX6IIncPSZOd0vbivd5PEsSN2PHE2h9WkFEncXKR9T+UcTOjV11Q0Y9fByPw/1
+1tEcRcj5HH58Ix2ZS7/w
+-----END PRIVATE KEY-----
diff --git a/plugin-tester-scala/src/main/resources/certs/domain.ext 
b/plugin-tester-scala/src/main/resources/certs/domain.ext
new file mode 100644
index 00000000..45324cc7
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/domain.ext
@@ -0,0 +1,5 @@
+authorityKeyIdentifier=keyid,issuer
+basicConstraints=CA:FALSE
+subjectAltName = @alt_names
+[alt_names]
+DNS.1 = localhost
diff --git a/plugin-tester-scala/src/main/resources/certs/localhost-server.crt 
b/plugin-tester-scala/src/main/resources/certs/localhost-server.crt
new file mode 100644
index 00000000..0b309d25
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/localhost-server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIJAJxuw7Wkhgc5MA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNV
+BAYTAlNFMRQwEgYDVQQHDAtFeGFtcGxldG93bjEUMBIGA1UECgwLRXhhbXBsZSBJ
+bmMwIBcNMjMwNTAyMTM0NzM4WhgPMjEyMzA0MDgxMzQ3MzhaMBQxEjAQBgNVBAMM
+CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANBsMucJ
+duEpgqi8RjQfR2gr/exaH0lS8GC2fzcDDrcuUJTbuqYX09Hea0Qg/Nx5HsSyhdN0
+HI3jFum/boOIGQtskrkId6vQUa1gZEws/qkPdQEcz2sY/G41UNSLZESzGaRhYvvD
+PjydmCUdBftV5gHoDKy0vdHIP44FRIwz/xeVEc/W5QcPa8DDuQDURHZyGYeAA6ZC
+5yQicNo7J/n85xx6VMDy6V9PpB6UR0id0v2luSyCKoIhoKxfVnU5aRN6QjHfLvys
+G8kyqjTFd11TYb4B99eC6uN0IutMTSvvxqzuhM8vZ38Ck4HjqO4qBy4D9w6RxZnP
+4zTksPqlYwNaeaUCAwEAAaN4MHYwUwYDVR0jBEwwSqE9pDswOTELMAkGA1UEBhMC
+U0UxFDASBgNVBAcMC0V4YW1wbGV0b3duMRQwEgYDVQQKDAtFeGFtcGxlIEluY4IJ
+AMm3mmQCkdyOMAkGA1UdEwQCMAAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqG
+SIb3DQEBCwUAA4IBAQBENcH6yNMywa7tBtelqADzds+yryVEGPNLkGABGdwzDxSm
+6tHZXdl1saT+AjhkoEFDABWhhqSkEMbObLIFtKpadtsRwq0OO1CoM7Bv0nsv5jSO
+s4mwdF4pO+4CxZARjQmfOXISiv6peDnmFlxK/sUDJUmUsY0gy6cZF4O+1NrQn4Np
+ew860WvR6L4JT80oDzKzqkHz52RzaRq1QmcvdohVVy8C8A9YrdIbKgQUhugIsVsq
+OOqzfBFcqkeywzdzWq5VmBS0bjNNz5+wpPKsCk7eXeFRLE/awkD2GeZoLw3LDPEg
+YdvNxWzymoNlB/MiP4HdGOK1DNcHLuwyOGkcNkV7
+-----END CERTIFICATE-----
diff --git a/plugin-tester-scala/src/main/resources/certs/localhost-server.key 
b/plugin-tester-scala/src/main/resources/certs/localhost-server.key
new file mode 100644
index 00000000..25c1f340
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/localhost-server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDQbDLnCXbhKYKo
+vEY0H0doK/3sWh9JUvBgtn83Aw63LlCU27qmF9PR3mtEIPzceR7EsoXTdByN4xbp
+v26DiBkLbJK5CHer0FGtYGRMLP6pD3UBHM9rGPxuNVDUi2REsxmkYWL7wz48nZgl
+HQX7VeYB6AystL3RyD+OBUSMM/8XlRHP1uUHD2vAw7kA1ER2chmHgAOmQuckInDa
+Oyf5/OccelTA8ulfT6QelEdIndL9pbksgiqCIaCsX1Z1OWkTekIx3y78rBvJMqo0
+xXddU2G+AffXgurjdCLrTE0r78as7oTPL2d/ApOB46juKgcuA/cOkcWZz+M05LD6
+pWMDWnmlAgMBAAECggEBALCrBnrQivRRO2/MJ7YGzYB/yb2OpvaAV0GjcDIxZUfg
++m0z1AL2L5a18jbNv4kjIfGZYdbblViwJbv9iK/1rUUBw10U0FvTOWi9TEdF3Jdx
+grxur2MYyuCgUOPZRCT3q8SqyDygQyEedNkAwRFKvqzfBd9fVYd9NmIsFO7DJHfX
+XFUJuxz4z0IrxW5XsTQ7PLPtbcz3hZYF6GSZlCBAw6A7rKCGvjqOXmRFuz5t0tk5
+HqIBEFcMaGFQOhxsX9L5U6LseuKKj30A6OFsd1yLa5r42towaXoSY0XveRiaAYRu
+4N1+zETCCQE/HHMNYfMpNDz6AtkErcDs28TFqIlSDAECgYEA8JlqSa+8umimP6cj
+3aft1zZ9M+voCKntxkIcdJ1dS8MfrqFwTZz5pMzwKYtdCT8Dhw4ZyOA1Stxg2pPF
+cW7XIHhMSlWRdfnrEGwHPGgZ2L5k9MtHZrSYCCJCSqqZjOSbosEfpuyzDTqgA0+S
+rHMYXkoWFRAThRu3yIHK41X3PhcCgYEA3cOFTnsQuCaW81TWAeKpfWFVPJVRDQOX
+LjuAprVNJ+EGiMuDWfvKFcuTdG4FuOawY9ZMbQYRNEZLoSGSEU4bSR2OCXMr0Wyy
+fG2C1cFN2QrylrhrkrvbFajOJK4UCnQLof92yRuuuUlKlqRMDN2rtcorLJ3BpSae
+noK6JHmBN6MCgYEAo2XNRVXQOliv7zK3rOVLJYmf5g8keh3NmYN0h84Helh9v79r
+4YnmEQINaGl5ObpNzv7IjB+YkcqxDECnKq4385k/VoxeSVz9Qx3anC+mvggvz//t
+8dZcGcoKc2MA/SqUeCfoMxk1UJqr6RO1bOCNgBuYe517ZD66xbU/8LyFOOkCgYEA
+02VTiSmFGZYnpRPE4Y049j03bIYF+jrm/XpZPBFt2EsI2JPvxXJhBH/IM1/B8q1t
+je41cmQrOEKeS55dyENFfWBACsAQEBXm2vfllXAsjm6CK6znVrver3n38D1E+2X9
+xNJqYHEUEKpOAOXjXQxeZ++tUl2bv5vd7so9ORHeXLMCgYEAkngbi1raD6RDRj+3
+8wvN/fl9s37l+9yCfj7VfeqQ899ypEBbPhFZtpTmkFtratMTEE2je4YxB4av45Zt
+Obzd1nBcWNyuEEq2sSxfjUnS3ruAEFM8aLK4mOgsm9mtni6OVel9BI9qY2sCWNXr
+BABBs7f/lr//lKFjor/fYIB1UsY=
+-----END PRIVATE KEY-----
diff --git a/plugin-tester-scala/src/main/resources/certs/rootCA.crt 
b/plugin-tester-scala/src/main/resources/certs/rootCA.crt
new file mode 100644
index 00000000..c2e4953d
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/rootCA.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC8DCCAdgCCQDJt5pkApHcjjANBgkqhkiG9w0BAQsFADA5MQswCQYDVQQGEwJT
+RTEUMBIGA1UEBwwLRXhhbXBsZXRvd24xFDASBgNVBAoMC0V4YW1wbGUgSW5jMCAX
+DTIzMDUwMjEzNDQ1OFoYDzIxMjMwNDA4MTM0NDU4WjA5MQswCQYDVQQGEwJTRTEU
+MBIGA1UEBwwLRXhhbXBsZXRvd24xFDASBgNVBAoMC0V4YW1wbGUgSW5jMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrDswRWKKj/HGl9IFTX1Ux4+3cc+
+XMuDdnA70sixEtWPKPRvgJqRb8WZd6ED6BPnZltWppAPB8AwHm3IDdMaEtGIKup0
+NaWZf2d+lqA/5caayGfZpu1IYUFJICAq8SdlF5LOwXWznP+g7632tRukAJk5IoC9
+F5C6GxExqL38waiLUj7FrQaw0vMi9lDS30pPVK4g3msoaD60qI2SjtPL0qO8iP6Z
+FUI79J7ugZNgFYUaxDRVIKCG2pknTcD2nx+n1AX+tcyQN4ybf9aIv5TyoE5Yki6I
++k2a3sDNzouxLTKPlgt5iCqU0440PfkYhP1rJ5p8cUEmwaSLaHUaRRu8YwIDAQAB
+MA0GCSqGSIb3DQEBCwUAA4IBAQAVAOmoL+wMIoqDgTfqwLsEGA7FiE1HerDE0mEv
+LcMECVavawexrsl2G4dao+YTMaRC0761aoIrxRoh8nm/jxly+ZWYPNXRVlMRtfMx
+WVALEOHhVJH/83swQzVNbuk/kz91Jeg0VS90OAw6QeO3ELg5HKqxdjqxr1+Emsz1
+q47dK2BWIMA5ux+pzL9jf1KVrx/tX3lnZH1Fr2Pup/l3FrHeLO+N6gFUirgiTebH
+JadlTQu+wM+CNVWy2n8tZLjkWZVPdI+D/WBe8wEznMHG1l4FOqsTO3FuxYCsFB+T
+M0YwXvSI+kjljc9ZEKEmuA1xwZqIt1MEJ0K7crSlMl55j1K6
+-----END CERTIFICATE-----
diff --git a/plugin-tester-scala/src/main/resources/certs/rootCA.key 
b/plugin-tester-scala/src/main/resources/certs/rootCA.key
new file mode 100644
index 00000000..c8ae5daa
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/certs/rootCA.key
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQI+9FLeLL2kl0CAggA
+MB0GCWCGSAFlAwQBKgQQr00F6ZDUAs2plMALi3ELbgSCBNDhYZP8LFd4VfJ4rQcc
+BDojlRvxdW5GlgGpNaaXcNDZjza2dWJuwik60reZizL6ldOUF8QCAal+J/r8gzNt
+3JbGaGmEgzTYvmusLab1JwHJwH7tJWwBNTihtTYwysr9KQ5r02uw5a4UcvbQilL0
+mC1Iz6ZJMS0YNWK64hFmcbP/niO9a8AHvNt8E1TedM/8E12SBnXNoClHs12FAebf
+AccIO8ZJVBS5l9+Zffajem9FVKrd2Zb6ymC3YJ8513pdFsx7TgxsAQv2nIMZWnh4
+j9l/cYoDONhg2VwNyYiDlzAua15OSg99Jz4LtNILD9VbRHdeBkg/gcInbjoqzMkD
+jPHJ6PUSiYgRDMwmSbNTDEnHiUUEsj2VqE8JyxHLYU1EV90ceq4YtckgdO2QUCT6
++kKnC3Fi/v/7Pk1JHCCGLTarJyYKQMhw3zOZe0nPndBOeWwf0Pajlf+iCz7SKEyr
+JG8o+anad9VzksoRpj5U+aP6OnyjjW+UfwBxztyfrcIRCpGoyEY8pLC4sOc7J2IQ
+jCLODCkQu8lM3ILqK3Ig4I6A44JYnYHQbP9pZz9rpt6Hqi/CTKWsuRtmDjYjmzj8
+ZHXogv+BlgJA20fQ5l7M3NXotdhX94A0lqDWN80LIyoDBSIgxw+pnaYT2QYIfY1b
+3O4N1cuTFdLOpNKhO2iOY/hA1fGgea4uGTkT0fWshuP9Sxf8+P+WCVdQxxGasMiS
+DWn9D861eSHZ/bq2Lbyy/6kjSOquzRDf8rO72i9Puxl16KRybiOSuRR+mNKf3zCk
+tiXSwttXSSrdhSF/sZsYHSq6sFmqw9HLKYXBAWAyIC9jqHK3zUM3xNMevDXHg78w
+pGHu7fNWICZD7mlJRxGXxArWs/YltrnOAXmlUemA+8N1cqJX2On+f41ow+XEV9Va
+lLwOBg4r03MkDxJEuwIneKzPxI26MzZTN8ppAnugpylhHiRpo577edAK6yNghyYt
+d8FyuBAprmi7zIrDXJLYmlSqmkwJemKo73fOTX6z6ravH+SY0G+24rzJXlsIHyDI
+JoY2EmbvD6bhhjKv55+s7kBG3HjC7S6Hj8BcXrZfFaje/YrNbUjHOpGV/j/APXlX
+9157K8XFH11Yh7xOzN1P3wA1+AK0+i6p273HqoH4MGk2eKxv0VWz5xpnKlwe10Ao
+uQMbLkggWVY/7L26X1v4af/YAerau2bDMwXxjs/2/O1CB9bbvL88z7DwkCNZq1JT
+lAgrbj+pkbduXO34WAUbmLajdLXiydhcyshTOYhFHka2dKnROWaoalSsKD6bqxRH
+MlhO56i2QmikLhzrNqHpyvQKaD1WG1tGM2U0IEAPQEo/jUZEeV+l1gkVHK8GaZtq
+TpzBx7q6qHnTesjYVoBxtCI/wdx5JBxS7VyCYmokSI24ATgHmMonroV3kNrjNoWA
+d7MqOALmWakQcW66NwNO+airTfHC2sHespO/tLpO5cP3cdsnV/pQwUgkfEmsc55p
+Hsqi3mFE226nttz8M63km7ch5p5kI6aNhFVkd2wLzHomlNwHCE+z9rDaXEdDcphy
++zyrHKN4qpMgq0PFYZ/V+sS3APzrdEGmD7g/IGaGO/QBwQwDchUgWYt8sKGe6TUu
+bboa0n7bo9vN8U3I66JoYP68tA==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/plugin-tester-scala/src/main/resources/logback.xml 
b/plugin-tester-scala/src/main/resources/logback.xml
new file mode 100644
index 00000000..87102bb3
--- /dev/null
+++ b/plugin-tester-scala/src/main/resources/logback.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- This is a development logging configuration that logs to standard 
out, for an example of a production
+        logging config, see the Pekko docs: 
https://pekko.apache.org/docs/pekko/current/typed/logging.html#logback -->
+    <appender name="STDOUT" target="System.out" 
class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>[%date{ISO8601}] [%level] [%logger] [%thread] 
[%X{pekkoSource}] - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+        <queueSize>1024</queueSize>
+        <neverBlock>true</neverBlock>
+        <appender-ref ref="STDOUT" />
+    </appender>
+
+    <root level="INFO">
+        <appender-ref ref="ASYNC"/>
+    </root>
+
+</configuration>
diff --git 
a/plugin-tester-scala/src/main/scala/example/myapp/helloworld/MtlsGreeterClient.scala
 
b/plugin-tester-scala/src/main/scala/example/myapp/helloworld/MtlsGreeterClient.scala
new file mode 100644
index 00000000..e764251b
--- /dev/null
+++ 
b/plugin-tester-scala/src/main/scala/example/myapp/helloworld/MtlsGreeterClient.scala
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2018-2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+//#full-client
+package example.myapp.helloworld
+
+import org.apache.pekko
+import pekko.actor.ActorSystem
+import pekko.grpc.GrpcClientSettings
+import pekko.pki.pem.{ DERPrivateKeyLoader, PEMDecoder }
+import example.myapp.helloworld.grpc.GreeterServiceClient
+import example.myapp.helloworld.grpc.HelloRequest
+
+import java.security.{ KeyStore, SecureRandom }
+import java.security.cert.{ Certificate, CertificateFactory }
+import javax.net.ssl.{ KeyManagerFactory, SSLContext, TrustManagerFactory }
+import scala.concurrent.ExecutionContext
+import scala.io.Source
+import scala.util.Success
+import scala.util.Failure
+
+object MtlsGreeterClient {
+
+  def main(args: Array[String]): Unit = {
+    implicit val sys: ActorSystem = ActorSystem.create("MtlsHelloWorldClient")
+    implicit val ec: ExecutionContext = sys.dispatcher
+
+    val clientSettings = GrpcClientSettings.connectToServiceAt("localhost", 
8443).withSslContext(sslContext())
+
+    val client = GreeterServiceClient(clientSettings)
+
+    val reply = client.sayHello(HelloRequest("Jonas"))
+
+    reply.onComplete { tryResponse =>
+      tryResponse match {
+        case Success(reply) =>
+          println(s"Successful reply: $reply")
+        case Failure(exception) =>
+          println("Request failed")
+          exception.printStackTrace()
+      }
+      sys.terminate()
+    }
+  }
+
+  def sslContext(): SSLContext = {
+    val clientPrivateKey =
+      
DERPrivateKeyLoader.load(PEMDecoder.decode(classPathFileAsString("certs/client1.key")))
+    val certFactory = CertificateFactory.getInstance("X.509")
+
+    // keyStore is for the client cert and private key
+    val keyStore = KeyStore.getInstance("PKCS12")
+    keyStore.load(null)
+    keyStore.setKeyEntry(
+      "private",
+      clientPrivateKey,
+      // No password for our private client key
+      new Array[Char](0),
+      
Array[Certificate](certFactory.generateCertificate(getClass.getResourceAsStream("/certs/client1.crt"))))
+    val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
+    keyManagerFactory.init(keyStore, null)
+    val keyManagers = keyManagerFactory.getKeyManagers
+
+    // trustStore is for what server certs the client trust
+    val trustStore = KeyStore.getInstance("PKCS12")
+    trustStore.load(null)
+    // accept any server cert signed by this CA
+    trustStore.setEntry(
+      "rootCA",
+      new KeyStore.TrustedCertificateEntry(
+        
certFactory.generateCertificate(getClass.getResourceAsStream("/certs/rootCA.crt"))),
+      null)
+    val tmf = TrustManagerFactory.getInstance("SunX509")
+    tmf.init(trustStore)
+    val trustManagers = tmf.getTrustManagers
+
+    val context = SSLContext.getInstance("TLS")
+    context.init(keyManagers, trustManagers, new SecureRandom())
+    context
+  }
+
+  private def classPathFileAsString(path: String): String =
+    Source.fromResource(path).mkString
+
+}
+//#full-client
diff --git 
a/plugin-tester-scala/src/main/scala/example/myapp/helloworld/MtlsGreeterServer.scala
 
b/plugin-tester-scala/src/main/scala/example/myapp/helloworld/MtlsGreeterServer.scala
new file mode 100644
index 00000000..3986c55a
--- /dev/null
+++ 
b/plugin-tester-scala/src/main/scala/example/myapp/helloworld/MtlsGreeterServer.scala
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2018-2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+//#full-server
+package example.myapp.helloworld
+
+import org.apache.pekko
+import pekko.actor.ActorSystem
+import pekko.http.scaladsl.ConnectionContext
+import pekko.http.scaladsl.Http
+import pekko.http.scaladsl.HttpsConnectionContext
+import pekko.http.scaladsl.model.HttpRequest
+import pekko.http.scaladsl.model.HttpResponse
+import pekko.pki.pem.DERPrivateKeyLoader
+import pekko.pki.pem.PEMDecoder
+import example.myapp.helloworld.grpc._
+import org.slf4j.LoggerFactory
+
+import java.security.KeyStore
+import java.security.SecureRandom
+import java.security.cert.Certificate
+import java.security.cert.CertificateFactory
+import javax.net.ssl.KeyManagerFactory
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManagerFactory
+import scala.concurrent.ExecutionContext
+import scala.concurrent.Future
+import scala.io.Source
+
+object MtlsGreeterServer {
+  def main(args: Array[String]): Unit = {
+    val system = ActorSystem("MtlsHelloWorldServer")
+    new MtlsGreeterServer(system).run()
+    // ActorSystem threads will keep the app alive until `system.terminate()` 
is called
+  }
+
+}
+
+class MtlsGreeterServer(system: ActorSystem) {
+
+  private val log = LoggerFactory.getLogger(classOf[MtlsGreeterServer])
+  def run(): Future[Http.ServerBinding] = {
+    // Pekko boot up code
+    implicit val sys: ActorSystem = system
+    implicit val ec: ExecutionContext = sys.dispatcher
+
+    // Create service handlers
+    val service: HttpRequest => Future[HttpResponse] =
+      GreeterServiceHandler(new GreeterServiceImpl())
+
+    // Bind service handler servers to localhost:8443
+    val binding =
+      Http().newServerAt("127.0.0.1", 
8443).enableHttps(serverHttpContext).bind(service)
+
+    // report successful binding
+    binding.foreach { binding => log.info(s"gRPC server bound to: {}", 
binding.localAddress) }
+
+    binding
+  }
+
+  private def serverHttpContext: HttpsConnectionContext = {
+    val certFactory = CertificateFactory.getInstance("X.509")
+
+    // keyStore/keymanagers are for the server cert and private key
+    val keyStore = KeyStore.getInstance("PKCS12")
+    keyStore.load(null)
+    val serverCert = 
certFactory.generateCertificate(getClass.getResourceAsStream("/certs/localhost-server.crt"))
+    val serverPrivateKey =
+      
DERPrivateKeyLoader.load(PEMDecoder.decode(classPathFileAsString("certs/localhost-server.key")))
+    keyStore.setKeyEntry(
+      "private",
+      serverPrivateKey,
+      // No password for our private key
+      new Array[Char](0),
+      Array[Certificate](serverCert))
+    val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
+    keyManagerFactory.init(keyStore, null)
+    val keyManagers = keyManagerFactory.getKeyManagers
+
+    // trustStore/trustManagers are for what client certs the server trust
+    val trustStore = KeyStore.getInstance("PKCS12")
+    trustStore.load(null)
+    // any client cert signed by this CA is allowed to connect
+    trustStore.setEntry(
+      "rootCA",
+      new KeyStore.TrustedCertificateEntry(
+        
certFactory.generateCertificate(getClass.getResourceAsStream("/certs/rootCA.crt"))),
+      null)
+    /*
+    // or specific client cert (probably less useful)
+    trustStore.setEntry(
+      "client",
+      new KeyStore.TrustedCertificateEntry(
+        
certFactory.generateCertificate(getClass.getResourceAsStream("/certs/client1.crt"))),
+      null)
+     */
+    val tmf = TrustManagerFactory.getInstance("SunX509")
+    tmf.init(trustStore)
+    val trustManagers = tmf.getTrustManagers
+
+    ConnectionContext.httpsServer { () =>
+      val context = SSLContext.getInstance("TLS")
+      context.init(keyManagers, trustManagers, new SecureRandom)
+
+      val engine = context.createSSLEngine()
+      engine.setUseClientMode(false)
+
+      // require client certs
+      engine.setNeedClientAuth(true)
+
+      engine
+    }
+  }
+
+  private def classPathFileAsString(path: String): String =
+    Source.fromResource(path).mkString
+}
+//#full-server
diff --git 
a/plugin-tester-scala/src/test/scala/example/myapp/helloworld/MtlsIntegrationSpec.scala
 
b/plugin-tester-scala/src/test/scala/example/myapp/helloworld/MtlsIntegrationSpec.scala
new file mode 100644
index 00000000..f7c266c0
--- /dev/null
+++ 
b/plugin-tester-scala/src/test/scala/example/myapp/helloworld/MtlsIntegrationSpec.scala
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package example.myapp.helloworld
+
+import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
+import org.apache.pekko.grpc.GrpcClientSettings
+import example.myapp.helloworld.grpc.{ GreeterServiceClient, HelloRequest }
+import org.scalatest.concurrent.ScalaFutures
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpecLike
+
+class MtlsIntegrationSpec
+    extends ScalaTestWithActorTestKit("pekko.http.server.enable-http2 = true")
+    with AnyWordSpecLike
+    with Matchers
+    with ScalaFutures {
+
+  "A mTLS server and client" should {
+
+    "be able to talk" in {
+      val server = new MtlsGreeterServer(system.classicSystem)
+      val serverBinding = server.run().futureValue
+
+      try {
+        val clientSettings =
+          GrpcClientSettings.connectToServiceAt("localhost", 
8443).withSslContext(MtlsGreeterClient.sslContext())
+
+        val client = GreeterServiceClient(clientSettings)
+
+        client.sayHello(HelloRequest("Jonas")).futureValue
+
+      } finally {
+        serverBinding.unbind().futureValue
+      }
+    }
+
+  }
+}
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 13916b88..f443278f 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -130,6 +130,7 @@ object Dependencies {
   lazy val pluginTester = l ++= Seq(
     // usually automatically added by `suggestedDependencies`, which doesn't 
work with ReflectiveCodeGen
     Compile.grpcStub,
+    Runtime.logback,
     Test.scalaTest,
     Test.scalaTestPlusJunit,
     Protobuf.googleCommonProtos)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to