Mutual Authentication in Scala with Akka - scala

I would create a TLS Session in Scala using Akka with mutual authentication between a client and a server. I have created two CA certificate that have to trust the respective certificates incoming from the other part.
Could you give me an exemple of how implement this?
Thank you.

I created a github project which demonstrates mutual authentication with different kind of clients, including Akka. Please have a look here: https://github.com/Hakky54/mutual-tls-ssl
It contains a full example of loading the ssl material into the client and server
A summary what you need to do is:
For the client
Create a key and a certificate and load it into a keystore
Export the certificate
Create a separate keystore for trusted certificates and import server certificate
Load the two keystores into your http client
For the server
Create a key and a certificate and load it into a keystore
Export the certificate
Create a separate keystore for trusted certificates and import client certificate
Load the two keystores into your server
It is not really clear to me what kind of server you are using, but if you are using spring-boot the example configuration would be:
server:
port: 8443
ssl:
enabled: true
key-store: classpath:identity.jks
key-password: secret
key-store-password: secret
trust-store: classpath:truststore.jks
trust-store-password: secret
client-auth: need
Akka requires a pre-configured instance of SSLContext to be able to configure HTTPS. An example of creating a client with https options would be the code snippet below.
import akka.actor.ActorSystem;
import akka.http.javadsl.ConnectionContext;
import akka.http.javadsl.Http;
import akka.http.javadsl.HttpsConnectionContext;
import com.typesafe.config.ConfigFactory;
import javax.net.ssl.SSLContext;
import java.util.Optional;
class App {
public static void main(String[] args) {
ActorSystem actorSystem = ActorSystem.create(
App.class.getSimpleName(),
ConfigFactory.defaultApplication(App.class.getClassLoader())
);
SSLContext sslContext = ...; //Initialized SSLContext
Http http = Http.get(actorSystem);
HttpsConnectionContext httpsContext = ConnectionContext.https(
sslContext,
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of(sslContext.getDefaultSSLParameters()));
http.setDefaultClientHttpsContext(httpsContext);
}
}
There are couple of libraries which provides easy to use utility/factory/builder classes to help you to create a SSLContext.
Apache SSLContextBuilder
Jetty SslContextFactory
SSLContext-Kickstart
There could be a bunch other libraries which provide similar functionality, but I am only aware of these three. By the way the sslcontext-kickstart is a library which is maintained by me.
Below is an overview of four ways to load the keystores and create an SSLContext. Vanilla Java and by using the three libraries.
import io.netty.handler.ssl.SslContextBuilder;
import nl.altindag.sslcontext.SSLFactory;
import org.apache.http.ssl.SSLContextBuilder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import javax.net.ssl.*;
import java.io.File;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Objects;
class SslExample {
public static void main(String[] args) throws Exception {
//Traditional flow of creating sslContext
String keyStorePath = "keystore.p12";
String trustStorePath = "truststore.p12";
char[] keyStorePassword = "secret".toCharArray();
char[] trustStorePassword = "secret".toCharArray();
KeyStore keyStore = KeyStore.getInstance("PKCS12");
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try(InputStream keyStoreInputStream = SslExample.class.getClassLoader().getResourceAsStream(keyStorePath);
InputStream trustStoreInputStream = SslExample.class.getClassLoader().getResourceAsStream(trustStorePath)) {
Objects.requireNonNull(keyStoreInputStream);
Objects.requireNonNull(trustStoreInputStream);
keyStore.load(keyStoreInputStream, keyStorePassword);
trustStore.load(trustStoreInputStream, trustStorePassword);
}
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(keyManagers, trustManagers, new SecureRandom());
//creating sslContext with Apache SSLContextBuilder
SSLContext sslContext1 = SSLContextBuilder.create()
.loadKeyMaterial(new File("keystore.p12"), "secret".toCharArray(), "secret".toCharArray())
.loadTrustMaterial(new File("truststore.p12"), "secret".toCharArray())
.build();
//creating sslContext with Jetty SslContextFactory
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
sslContextFactory.setKeyStorePath("keystore.p12");
sslContextFactory.setKeyStorePassword("secret");
sslContextFactory.setTrustStorePath("truststore.p12");
sslContextFactory.setTrustStorePassword("secret");
sslContextFactory.start();
SSLContext sslContext2 = sslContextFactory.getSslContext();
//creating sslContext with sslcontext-kickstart
SSLFactory sslFactory = SSLFactory.builder()
.withIdentity("keystore.p12", "secret".toCharArray())
.withTrustStore("truststore.p12", "secret".toCharArray())
.build();
SSLContext sslContext3 = sslFactory.getSslContext();
}
}
It is in java, but IntelliJ Idea provides a handy translate function to scala when pasting the code snippet.

Related

Micronaut + Vertx + testcontainers

How do I configure Micronaut app using Vert.x and testcontainers? I'm trying:
application-test.yml
datasources:
default:
url: jdbc:tc:mysql:8:///db
driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver
vertx:
mysql:
client:
uri: jdbc:tc:mysql:8:///db
Tests with micronaut-data-jdbc work, but with micronaut-vertx-mysql-client not work:
Error:
Message: Cannot parse invalid connection URI: jdbc:tc:mysql:8:///db
I'm not very familiar with testecontainers, but it seems like it doesn't come up with a fixed port, so I don't know how to configure the connection URI.
Thanks!
It might be a problem that micronaut-vertx-mysql-client does not support the Testcontainers JDBC URL scheme (hard to say without further logs).
In this case, I would suggest to use Testcontainers with database container objects instead of the special JDBC URL.
I got a solution to the problem:
Micronaut + jdbc hikari + vertx mysql client + flyway mysql
package br.com.app;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.PropertySource;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.EmbeddedApplication;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
#Testcontainers
class AuthTest {
static Logger log = LoggerFactory.getLogger(AuthTest.class.getName());
#Container
static MySQLContainer mysql = new MySQLContainer("mysql:8");
private HttpClient client;
private static EmbeddedApplication application;
private static ApplicationContext context;
#BeforeAll
public static void initTests(){
log.info("Mysql is running {}, port {}", mysql.isRunning(), mysql.getFirstMappedPort());
var port = mysql.getFirstMappedPort();
var host = mysql.getHost();
var database = mysql.getDatabaseName();
var user = mysql.getUsername();
var password = mysql.getPassword();
var url = String.format("jdbc:mysql://%s:%s/%s", host, port, database);
application = ApplicationContext.run(EmbeddedApplication.class,
PropertySource.of(
"test",
CollectionUtils.mapOf(
"vertx.mysql.client.port", port,
"vertx.mysql.client.host", host,
"vertx.mysql.client.database", database,
"vertx.mysql.client.user", user,
"vertx.mysql.client.password", password,
"datasources.default.url", url,
"datasources.default.username", user,
"datasources.default.password", password,
"flyway.datasources.default.enabled", true
)
));
context = application.getApplicationContext();
}
#BeforeEach
void beforeEach(){
this.authService = context.getBean(AuthService.class);
this.client = context.getBean(HttpClient.class);
}
#Test
void testItWorks() {
Assertions.assertTrue(application.isRunning());
}
// api tests
}
Help links:
https://dev.to/major13ua/micronaut-integration-testing-using-testcontainers-2e30
https://github.com/major13ua/micronaut-tc/blob/main/src/test/java/com/example/testcontainer/controller/DemoControllerTest.java

Spring boot mongo tls cert

I have set up TLS in mongod.conf. I need to use spring boot to connect to my mongo which requires tls now. In MongoCompass, I set Certificate Authority, Client Certificate & Client Private Key to root-ca.pem, test.pem & test.pem accordingly and I am able to connect. How can i specify the root-ca.pem & test.pem correctly in mongoclientoptions to connect to my mongo?
This is my mongod.conf
# network interfaces
net:
port: 27017
bindIp: 127.0.0.1
tls:
mode: requireTLS
certificateKeyFile: C:\TLSServerMongo\test.pem
CAFile: C:\TLSServerMongo\root-ca.pem
clusterFile: C:\TLSServerMongo\test.pem
allowInvalidCertificates: true
FIPSMode : false
This is my mongoclientoptions
#Bean
public MongoClientOptions mongoClientOptions() {
MongoClientOptions.Builder mongoClientOptions = MongoClientOptions.builder().sslInvalidHostNameAllowed(true).sslEnabled(true);
try {
// String fileName = directory + RDS_COMBINED_CA_BUNDLE;
String fileName = "C:\\TLSServerMongo\\test.pem";
InputStream is = new FileInputStream(fileName);
// You could get a resource as a stream instead.
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate caCert = (X509Certificate) cf.generateCertificate(is);
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null); // You don't need the KeyStore instance to come from a file.
ks.setCertificateEntry("caCert", caCert);
tmf.init(ks);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
mongoClientOptions.sslContext(sslContext);
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
return mongoClientOptions.build();
}
This is my MongoClient
public #Bean
MongoClient mongoClient() {
List<MongoCredential> allCred = new ArrayList<>();
allCred.add(MongoCredential.createCredential(username, database, password.toCharArray()));
MongoClient client = new MongoClient((new ServerAddress(this.myHost, this.myPort)), allCred, mongoClientOptions());
client.setWriteConcern(WriteConcern.ACKNOWLEDGED);
return client;
}
It will be a Good idea to create one single .jks file as a cert and use it in spring boot mongo client...
pls refre to this for converting your .pem cert into JKS Convert .pem files to .jks
Once you have the .jks in your keystore on your system then we can use it or you may follow this example to connect using jks...
Connecting to MongoDB from spring boot app using ssl
or
https://dba.stackexchange.com/questions/206462/how-to-configure-ssl-mongodb-connection-in-yml-file-spring-boot
Hope it helps!

Akka. How to set a pem certificate in a https request

I'm using Akka (version 2.5.18) to send JSON strings to a specific server via https. I have used a poolRouter (balancing-pool with 10 instances) in order to create a pool of actors that are going to send JSONs (generated from different customers) to a single server:
val router: ActorRef = system.actorOf(
FromConfig.props(Props(new SenderActor(configuration.getString("https://server.com"), this.self))),
"poolRouter"
)
The project specification says that the requests can also be sent using curl:
curl -X PUT --cert certificate.pem --key private.key -H 'Content-Type: application / json' -H 'cache-control: no-cache' -d '[{"id" : "test"}] 'https://server.com'
Where "certificate.pem" is the tls certificate of the customer and "private.key" is the private key used to generate the CSR of the customer.
I'm using a balancing-pool because I will have a very big set of certificates (one for each customer) and I need to send the requests concurrently.
My approach is to have a "SenderActor" class that will be created by the balancing pool. Each actor, upon the reception of a message with a "customerId" and the JSON data generated by this customer, will send a https request:
override def receive: Receive = {
case Data(customerId, jsonData) =>
send(customerId(cid, jsonData))
Each SenderActor will read the certificate (and the private key) based on a path using the customerId. For instance, the customerId: "cust1" will have their certificate and key stored in "/home/test/cust1". This way, the same actor class can be used for all the customers.
According to the documentation, I need to create a HttpsConnectionContext in order to send the different requests:
def send(customerId: String, dataToSend): Future[HttpResponse] = {
// Create the request
val req = HttpRequest(
PUT,
uri = "https://server.com",
entity = HttpEntity(`application/x-www-form-urlencoded` withCharset `UTF-8`, dataToSend),
protocol = `HTTP/1.0`)
val ctx: SSLContext = SSLContext.getInstance("TLS")
val permissiveTrustManager: TrustManager = new X509TrustManager() {
override def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit = {}
override def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit = {}
override def getAcceptedIssuers(): Array[X509Certificate] = Array.empty
}
ctx.init(Array.empty, Array(permissiveTrustManager), new SecureRandom())
val httpsConnContext: HttpsConnectionContext = ConnectionContext.https(ctx)
// Send the request
Http(system).singleRequest(req, httpsConnContext)
}
The problem I have is that I don't have any clue about how to "set the certificate and the key" in the request, so that the server accepts them.
For instance, I can read the certificate using the following code:
import java.util.Base64
val certificate: String => String = (customer: String) => IO {
Source.fromInputStream(getClass.getClassLoader
.getResourceAsStream("/home/test/".concat(customer).concat("_cert.pem")))
.getLines().mkString
}.unsafeRunSync()
val decodedCertificate = Base64.getDecoder.decode(certificate(customerId)
.replaceAll(X509Factory.BEGIN_CERT, "").replaceAll(X509Factory.END_CERT, ""))
val cert: Certificate = CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(decodedCertificate))
But I don't know how to "set" this certificate and the private key in the request (which is protected by a passphrase), so that the server accepts it.
Any hint or help would be greatly appreciated.
The following allows making a https request and identifying yourself with a private key from a x.509 certificate.
The following libraries are used to manage ssl configuration and to make https calls:
ssl-config
akka-http
Convert your pem certificate to pks12 format as defined here
openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.crt
Define key-store in your application.conf. It supports only pkcs12 and because of
this step 1 is required.
ssl-config {
keyManager {
stores = [
{
type = "pkcs12"
path = "/path/to/pkcs12/cetificate"
password = changeme //the password is set when using openssl
}
]
}
}
Load ssl config using special akka trait DefaultSSLContextCreation
import akka.actor.ActorSystem
import akka.actor.ExtendedActorSystem
import akka.http.scaladsl.DefaultSSLContextCreation
import com.typesafe.sslconfig.akka.AkkaSSLConfig
import com.typesafe.sslconfig.ssl.SSLConfigFactory
class TlsProvider(val actorSystem: ActorSystem) extends DefaultSSLContextCreation {
override protected def sslConfig: AkkaSSLConfig =
throw new RuntimeException("Unsupported behaviour when creating new sslConfig")
def httpsConnectionContext() = {
val akkaSslConfig =
new AkkaSSLConfig(system.asInstanceOf[ExtendedActorSystem], SSLConfigFactory.parse(system.settings.config))
createClientHttpsContext(akkaSslConfig)
}
}
Create a https context and use in http connection pool.
Http(actorSystem).cachedHostConnectionPoolHttps[RequestContext](
host = host,
port = portValue,
connectionContext = new TlsProvider(actorSystem).httpsConnectionContext()
)
Or set connection context to Http(actorSystem).singleRequest method.
In summary, I used ssl-config library to manage certificates instead of doing it programmatically yourself. By defining a keyManager in a ssl-config, any http request done with help of custom httpsConnectionContext will use the certificate to identify the caller/client.
I focused on describing how to establish a https connection using client certificate. Any dynamic behavior for managing multiple certificates is omitted. But I hope this code should be able give you understanding how to proceed.

Issue in sending a SOAP request in ApacheHttpClient with SSL

I want to send a SOAP request through SSL to SOAP server (Microsoft IIS server). When I test the SOAP request through the soapUI tool with SSL - Keystore configurations it returns response correctly. But Using following code it returns "HTTP/1.1 400 Bad Request"
I used httpclient-4.2.3 and httpcore-4.2.2.
import java.security.KeyStore;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.BasicClientConnectionManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
public final static void main(String[] args) throws Exception {
DefaultHttpClient httpclient = new DefaultHttpClient();
try {
KeyStore keyStore = KeyStore.getInstance("JKS");
FileInputStream instream1 = new FileInputStream(new File("C:\\CCDKeyStore\\mykeystore.jks"));
try {
keyStore.load(instream1, "1214524".toCharArray());
} finally {
try { instream1.close(); } catch (Exception ignore) {}
}
SSLSocketFactory socketFactory = new SSLSocketFactory(keyStore,"1214524");
Scheme sch = new Scheme("https", 443, socketFactory);
SchemeRegistry schemeRegistry = httpclient.getConnectionManager().getSchemeRegistry();
schemeRegistry.register(sch);
final HttpParams httpParams = new BasicHttpParams();
DefaultHttpClient lHttpClient = new DefaultHttpClient(new BasicClientConnectionManager(schemeRegistry), httpParams);
String lUrl = "https://api.demo.server.com/XDS/Service.svc?wsdl";
StringBuffer lXmlBuffer = new StringBuffer();
lXmlBuffer.append("<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:a=\"http://www.w3.org/2005/08/addressing\">\n");
lXmlBuffer.append("<s:Header>\n");
lXmlBuffer.append("<a:Action s:mustUnderstand=\"1\">urn:74347:4757:StoredQuery</a:Action>\n");
lXmlBuffer.append("<a:MessageID>urn:uuid:c6430690-412e-4744-afe1-233e2138f2d2</a:MessageID>\n");
.
.
.
.
.
.
lXmlBuffer.append("</Slot>\n");
lXmlBuffer.append("</AdhocQuery>\n");
lXmlBuffer.append("</query:AdhocQueryRequest>\n");
lXmlBuffer.append("</s:Body>\n");
lXmlBuffer.append("</s:Envelope>\n");
String lXml = lXmlBuffer.toString();
HttpPost lMethod = new HttpPost(lUrl);
HttpEntity lEntity = new StringEntity(lXml, "multipart/related", "utf-8");
lMethod.setHeader("SOAPAction", "urn:74347:4757:StoredQuery");
System.out.println(EntityUtils.toString(lEntity));
lMethod.setEntity(lEntity);
HttpResponse lHttpResponse = lHttpClient.execute(lMethod);
} finally {
httpclient.getConnectionManager().shutdown();
}
Any help on this highly appreciate
Thanks,
Mohan
Finally I found the answer...
Here the following line in above code contains content type as "multipart/related". But web service is expected the content type "application/soap+xml" since this is SOAP request. Please refer w3schools tutorial for further information
HttpEntity lEntity = new StringEntity(lXml, "multipart/related", "utf-8");

Jersey Grizzly REST service not visible outside localhost

I'm trying to write a REST service in java using Jersey and Glassfish Grizzly. I have a very simple case working internally, but can't seem to call on the server from an external address. I've tried using a variety of different pairs of machines with externally visible IP's, and tried specifying the actual IP address in the server instead of localhost, but nothing works. I'm somewhat loosely following the official user guide here. My resource:
package resources;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
#Path("/simpleREST")
public class SimpleRESTResource
{
#GET
#Produces("text/plain")
public String getMessage()
{
return "Message from server\n";
}
}
And the server:
import java.io.IOException;
import java.net.URI;
import javax.ws.rs.core.UriBuilder;
import org.glassfish.grizzly.http.server.HttpServer;
import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
import com.sun.jersey.api.core.PackagesResourceConfig;
import com.sun.jersey.api.core.ResourceConfig;
public class Main
{
public static final URI BASE_URI = UriBuilder.fromUri("http://localhost").port(9998).build();
public static void main(String[] args) throws IOException
{
System.out.println("Starting grizzly...");
ResourceConfig rc = new PackagesResourceConfig("resources");
HttpServer myServer = GrizzlyServerFactory.createHttpServer(BASE_URI, rc);
System.out.println(String.format("Jersey app started with WADL available at %s/application.wadl\n" +
"Try out %s/simpleREST\nHit enter to stop it...", BASE_URI, BASE_URI));
System.in.read();
myServer.stop();
}
}
On the same machine, I can successfully interact with the server using
curl -X GET localhost:9998/simpleREST
OR
curl -X GET [external numeric address]:9998/simpleREST
Many thanks in advance for any suggestions.
SOLUTION
I have fixed this problem by setting the server URI to http://0.0.0.0:9998 instead of localhost, 127.0.0.1, or the actual address.
To make a server IP adress visible outside of localhost, you must fist open the neccessary firewall ports(if you have one), or use "0.0.0.0" instead of "localhost" in order for the server to listen to all IP addresses and network adapters. Before testing it in your local network, try pinging your server device from your client device to check if there is an actual connection or if the devices are not connected at all.
With Jersey and Grizzly 2.30, it's simpler:
final ResourceConfig rc = new ResourceConfig().packages("com.rest");
HttpServer httpServer = Grizzly
HttpServerFactory.createHttpServer(URI.create("http://0.0.0.0:9998/api/"), rc);
or, you can try these codes below:
ResourceConfig rc = new PackagesResourceConfig("your-rest-packages");
HttpHandler handler = ContainerFactory.createContainer(HttpHandler.class, rc);
server = new HttpServer();
server.getServerConfiguration().addHttpHandler(handler);
//attach listeners
InetAddress localHost = InetAddress.getLocalHost();
String localHostAddr = localHost.getHostAddress();
NetworkListener localHostListener = new NetworkListener("localhost", localHostAddr, port);
server.addListener(localHostListener);
InetAddress loopback = InetAddress.getLoopbackAddress();
String loopbackAddr = loopback.getHostAddress();
NetworkListener loopbackListener = new NetworkListener("loopback", loopbackAddr, port);
now your server could both list to localhost and loopback