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]