Background:
I have a Scala
(with AKKA-http
) webserver, that is able to talk to instances of itself (de-centralised model). I recently upgraded the server to use SSL
and https
.
I have run into a problem with the communications between two servers, related to SSL
certificates:
java.lang.RuntimeException: javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Thoughts:
A quick search for the exception lead me to this post. I had seen the keytool
command at various points when I was researching SSL
in Scala
/Java
so I know I'm on the right track.
However, the service I am writing needs to be end-user friendly. The answers in this post require a lot of user setup. I would like the user to have to do as little as possible in the way of setting this up and would like to have my server handle this itself.
The model here was you share your domain name and serverID with your friends, and they can register with you. The post mentioned would require you to also share your SSL certificate and manually add it on your machine for each server you register with.
I think I need to exchange the SSL
certificate at some point during the registration handshake?
Handshake Process:
Currently the handshake process is as follows:
RegistrationRequest(serverID, serverIP, alias)
to User B, and a database entry for User BRegistrationAccept
message to user Achatter
service is started on both endsI was thinking I would do the handshake with http
, inserting the SSL
certificate into the RegistrationRequest
object, and upgrade the connection to https
when the certificate was exchanged, but this is a problem. Each webserver binds to port 8080
. I would have to re-bind the entire socket each time which will work for just 1 to 1 peers, but not 1 to many.
Any suggestions on the correct approach to solving this?
Thanks in advance!
UPDATE:
I have come quite far with this project and integrated a lot of features into the backend. However, the system is meant to be security focused so if I continue this way, SSL
is a must.
I didn't really want to re-write the entire backend, but assuming I did, are websockets
what I want here instead of my current approach?
UPDATE 2:
Seems there are two approaches, neither of which I'm completely sure about (from a security perspective).
Approach 1:
I ship the software with a single RootCA
certificate, that I use to generate the SSL
certificates. This seems like it might have security implications if someone gets hold of the RootCA
and generates their own certificates.
Approach 2:
I create 2 sockets, one for http
(port 8080) and another for https
(port 8443).
The http
socket exposes routes ONLY for authenticating credentials (and providing a token) and for adding a certificate to the truststore
(ONLY if the token is valid).
Then any requests on the https
routes will be rejected if the certificate is not present or the token is invalid, but will work for registered users.
Again, seems to have security implications sharing certificate over http
, but seems better than shipping with RootCA
as it at least has authentication wrapped around it (or am I wrong?)
I think I have formed a plan.
I bind a connection using HTTP
to 8080
, and allow an auth route and a certificate route (protected by auth token).
Each instance encrpyts the certificate using the hashed password
The hashed password is used on the auth root to get an access_token
The instnace can then use this access_token to send its encrypted certificate to the other server, which gets put into the other servers truststore, allowing https to be used going forward.
I think this solves the concerns with a) shipping the same RootCA
and b) sending unencrypted certificates via http
.
I will implement this over the next couple of days. If someone spots any security concerns I have potentially missed with this, please comment otherwise I will mark it as the answer when I have tested it works.
UPDATE:
I have fully implemented this solution, and seems to work pretty well. When a server instance is first aware of another (through user input), it asks for an authToken
via http
, which it then uses to send it's encrypted certificate over http
to the other instance. The other instance responds with its encrypted certificate. Each side decrypts and stores the certificate in the truststore, allowing for full https
communications both ways.
The section in my SSLManager
objet for registering and encrypting/decrypting certificates:
def registerEncryptedCertificate(certificateResponse: CertificateResponse)(implicit actorSystem: akka.actor.ActorSystem): Boolean = {
var success = false
try {
val decryptedCertificateData = decrypt(Base64.getDecoder.decode(certificateResponse.encryptedCertificate), DatabaseUtil.hashString(PASSWORD))
val certificateString = String(decryptedCertificateData, "UTF-8")
// write the certificate to file if it doesn't exist
if (!fileExists(SSL_ROOT + s"${certificateResponse.serverID}.crt")) {
writeFile(SSL_ROOT + s"${certificateResponse.serverID}.crt", Seq(certificateString))
// load the certificate into truststore depending on platform
OS.getOS match {
case MAC =>
Seq("bash", "-c", s"keytool -import -v -trustcacerts -alias ${certificateResponse.serverID} " +
s"-file ${SSL_ROOT + certificateResponse.serverID}.crt " +
"-storepass changeit -noprompt -keystore $(/usr/libexec/java_home)/lib/security/cacerts") !!
success = true
case LINUX =>
Seq("bash", "-c", s"keytool -import -v trustcacerts -alias ${certificateResponse.serverID} " +
s"-file ${SSL_ROOT + certificateResponse.serverID}.crt " +
"-storepass changeit -noprompt -keystore $(dirname $(dirname $(readlink -f $(which javac))))/lib/security/cacerts") !!
success = true
//TODO: WINDOWS OS support
}
} else {
actorSystem.log.warning(s"Certificate for serverID:${certificateResponse.serverID} already exists.")
success = true
}
} catch {
case e: Exception =>
actorSystem.log.error(s"Something went wrong processing the certificateResponse:${certificateResponse.toProtoString}", e)
}
success
}
def registerEncryptedCertificate(certificateRequest: CertificateRequest)(implicit actorSystem: ActorSystem[_]): CertificateResponse = {
var result: CertificateResponse = CertificateResponse(null, SERVER_ID, false, "Failed to register certificate")
try {
val decryptedCertificateData = decrypt(Base64.getDecoder.decode(certificateRequest.encryptedCertificate), DatabaseUtil.hashString(PASSWORD))
val certificateString = String(decryptedCertificateData, "UTF-8")
// write the certificate to file if it doesn't exist
if (!fileExists(SSL_ROOT + s"${certificateRequest.serverID}.crt")) {
writeFile(SSL_ROOT + s"${certificateRequest.serverID}.crt", Seq(certificateString))
// load the certificate into truststore depending on platform
OS.getOS match {
case MAC =>
Seq("bash", "-c", s"keytool -import -v -trustcacerts -alias ${certificateRequest.serverID} " +
s"-file ${SSL_ROOT + certificateRequest.serverID}.crt " +
"-storepass changeit -noprompt -keystore $(/usr/libexec/java_home)/lib/security/cacerts") !!
val encryptedCertificateString = getEncryptedCertificate(PASSWORD)
result = CertificateResponse(encryptedCertificateString, SERVER_ID, true, "Successfully registered certificate")
case LINUX =>
Seq("bash", "-c", s"keytool -import -v trustcacerts -alias ${certificateRequest.serverID} " +
s"-file ${SSL_ROOT + certificateRequest.serverID}.crt " +
"-storepass changeit -noprompt -keystore $(dirname $(dirname $(readlink -f $(which javac))))/lib/security/cacerts") !!
val encryptedCertificateString = getEncryptedCertificate(PASSWORD)
result = CertificateResponse(encryptedCertificateString, SERVER_ID, true, "Successfully registered certificate")
//TODO: WINDOWS OS support
}
} else {
val encryptedCertificateString = getEncryptedCertificate(PASSWORD)
result = CertificateResponse(encryptedCertificateString, SERVER_ID, true, "Certificate already registered")
}
} catch {
case e: Exception =>
actorSystem.log.error(s"Something went wrong processing the certificateRequest:${certificateRequest.toProtoString}", e)
}
result
}
private def getEncryptedCertificate(password: String): String = {
// get our encrypted certificate to return
val certificateContents = readFile(SSL_CERTIFICATE_PATH)
val encryptedCertificateData = encrypt(certificateContents.getBytes, DatabaseUtil.hashString(password))
Base64.getEncoder.encodeToString(encryptedCertificateData)
}
def createCertificateRequest(password: String)(implicit actorSystem: akka.actor.ActorSystem): CertificateRequest = {
var result: CertificateRequest = null
try {
val encryptedCertificate = getEncryptedCertificate(password)
result = CertificateRequest(encryptedCertificate, SERVER_ID)
} catch {
case e: Exception =>
actorSystem.log.error(e, "Failed to create certificate request")
}
result
}
And the protos
used:
syntax = "proto3";
package api;
message CertificateRequest {
string encryptedCertificate = 1;
string serverID = 2;
}
syntax = "proto3";
package api;
message CertificateResponse {
string encryptedCertificate = 1;
string serverID = 2;
bool success = 3;
string message = 4;
}