Reputation: 321
I am having a problem establishing a Client-Server Socket connection using SSL Sockets. I am still in the learning process of SSL encryption and dealing with certificates and keystores. I have a client and server application that are supposed to connect to one another as follows:
SERVER - SSLReverseEchoer.java
import java.io.*;
import java.net.*;
import java.security.*;
import javax.net.ssl.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class SSLReverseEchoer {
public static void main(String[] args) {
String ksName = "sslkeystore.jks";
String keystorePass = "sslkeystorepassword";
char ksPass[] = keystorePass.toCharArray();
int sslPort = 9099;
Security.addProvider(new BouncyCastleProvider());
File file = new File(ksName);
String absPath = file.getAbsolutePath();
System.setProperty("javax.net.ssl.keyStore", absPath);
System.setProperty("javax.net.ssl.keyStorePassword", "sslkeystorepassword");
try {
//Get keystore w/ password
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(ksName), ksPass);
//Trust Manager
TrustManagerFactory tmf = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, ksPass);
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
SSLServerSocketFactory ssf = sc.getServerSocketFactory();
SSLServerSocket s = (SSLServerSocket) ssf.createServerSocket(sslPort);
printServerSocketInfo(s);
//WAIT FOR CONNECTION TODO ADD THREAD for NIO
SSLSocket c = (SSLSocket) s.accept();
printSocketInfo(c);
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(
c.getOutputStream()));
BufferedReader r = new BufferedReader(new InputStreamReader(
c.getInputStream()));
String m = "Welcome to SSL Reverse Echo Server."+
" Please type in some words.";
w.write(m,0,m.length());
w.newLine();
w.flush();
while ((m=r.readLine())!= null) {
if (m.equals(".")) break;
char[] a = m.toCharArray();
int n = a.length;
for (int i=0; i<n/2; i++) {
char t = a[i];
a[i] = a[n-1-i];
a[n-i-1] = t;
}
w.write(a,0,n);
w.newLine();
w.flush();
}
w.close();
r.close();
c.close();
s.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void printSocketInfo(SSLSocket s) {
System.out.println("Socket class: "+s.getClass());
System.out.println(" Remote address = "
+s.getInetAddress().toString());
System.out.println(" Remote port = "+s.getPort());
System.out.println(" Local socket address = "
+s.getLocalSocketAddress().toString());
System.out.println(" Local address = "
+s.getLocalAddress().toString());
System.out.println(" Local port = "+s.getLocalPort());
System.out.println(" Need client authentication = "
+s.getNeedClientAuth());
SSLSession ss = s.getSession();
System.out.println(" Cipher suite = "+ss.getCipherSuite());
System.out.println(" Protocol = "+ss.getProtocol());
}
private static void printServerSocketInfo(SSLServerSocket s) {
System.out.println("Server socket class: "+s.getClass());
System.out.println(" Socket address = "
+s.getInetAddress().toString());
System.out.println(" Socket port = "
+s.getLocalPort());
System.out.println(" Need client authentication = "
+s.getNeedClientAuth());
System.out.println(" Want client authentication = "
+s.getWantClientAuth());
System.out.println(" Use client mode = "
+s.getUseClientMode());
}
}
CLIENT - SSLSocketClient.java
import java.io.*;
import java.net.*;
import javax.net.ssl.*;
public class SSLSocketClient {
public static void main(String[] args) {
int sslPort = 9099;
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintStream out = System.out;
SSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault();
try {
SSLSocket c = (SSLSocket) f.createSocket("localhost", sslPort);
printSocketInfo(c);
c.startHandshake();
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(c.getOutputStream()));
BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream()));
String m = null;
while ((m=r.readLine())!= null) {
out.println(m);
m = in.readLine();
w.write(m,0,m.length());
w.newLine();
w.flush();
}
w.close();
r.close();
c.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void printSocketInfo(SSLSocket s) {
System.out.println("Socket class: "+s.getClass());
System.out.println(" Remote address = "
+s.getInetAddress().toString());
System.out.println(" Remote port = "+s.getPort());
System.out.println(" Local socket address = "
+s.getLocalSocketAddress().toString());
System.out.println(" Local address = "
+s.getLocalAddress().toString());
System.out.println(" Local port = "+s.getLocalPort());
System.out.println(" Need client authentication = "
+s.getNeedClientAuth());
SSLSession ss = s.getSession();
System.out.println(" Cipher suite = "+ss.getCipherSuite());
System.out.println(" Protocol = "+ss.getProtocol());
}
}
I execute the SERVER and it begins listening on the specified port, but when the CLIENT is executed I get the following stacktraces.
SERVER OUTPUT
Server socket class: class sun.security.ssl.SSLServerSocketImpl
Socket address = 0.0.0.0/0.0.0.0
Socket port = 9099
Need client authentication = false
Want client authentication = false
Use client mode = false
Socket class: class sun.security.ssl.SSLSocketImpl
Remote address = /127.0.0.1
Remote port = 62145
Local socket address = /127.0.0.1:9099
Local address = /127.0.0.1
Local port = 9099
Need client authentication = false
Cipher suite = SSL_NULL_WITH_NULL_NULL
Protocol = NONE
javax.net.ssl.SSLException: Connection has been shutdown: javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
at sun.security.ssl.SSLSocketImpl.checkEOF(SSLSocketImpl.java:1541)
at sun.security.ssl.SSLSocketImpl.checkWrite(SSLSocketImpl.java:1553)
at sun.security.ssl.AppOutputStream.write(AppOutputStream.java:71)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:295)
at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:141)
at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:229)
at java.io.BufferedWriter.flush(BufferedWriter.java:254)
at com.example.SSLReverseEchoer.main(SSLReverseEchoer.java:60)
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
at sun.security.ssl.Alerts.getSSLException(Alerts.java:154)
at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:2023)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1125)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
at sun.security.ssl.SSLSocketImpl.getSession(SSLSocketImpl.java:2267)
at com.example.SSLReverseEchoer.printSocketInfo(SSLReverseEchoer.java:94)
at com.example.SSLReverseEchoer.main(SSLReverseEchoer.java:49)
CLIENT OUTPUT
Socket class: class sun.security.ssl.SSLSocketImpl
Remote address = localhost/127.0.0.1
Remote port = 9099
Local socket address = /127.0.0.1:62145
Local address = /127.0.0.1
Local port = 62145
Need client authentication = false
Cipher suite = SSL_NULL_WITH_NULL_NULL
Protocol = NONE
javax.net.ssl.SSLException: Connection has been shutdown: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.SSLSocketImpl.checkEOF(SSLSocketImpl.java:1541)
at sun.security.ssl.SSLSocketImpl.checkWrite(SSLSocketImpl.java:1553)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1399)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
at com.example.SSLSocketClient.main(SSLSocketClient.java:23)
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1509)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:914)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
at sun.security.ssl.SSLSocketImpl.getSession(SSLSocketImpl.java:2267)
at com.example.SSLSocketClient.printSocketInfo(SSLSocketClient.java:55)
at com.example.SSLSocketClient.main(SSLSocketClient.java:22)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:387)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)
at sun.security.validator.Validator.validate(Validator.java:260)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1491)
... 9 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:382)
... 15 more
I used keystore to generate sslkeystore.jks and X509_certificate.cer with the following parameters:
Generate sslkeystore.jks
keytool -genkey -keyalg RSA -alias sslsocket -keystore sslkeystore.jks -storepass sslkeystorepassword -validity 365 -keysize 2048
What is your first and last name?
[Unknown]: Gandalf
What is the name of your organizational unit?
[Unknown]: Wizardry
What is the name of your organization?
[Unknown]: Arnock
What is the name of your City or Locality?
[Unknown]: Minas Tirith
What is the name of your State or Province?
[Unknown]: Gondor
What is the two-letter country code for this unit?
[Unknown]: GD
Is CN=Gandalf, OU=Wizardry, O=Arnock, L=Minas Tirith, ST=Gondor, C=GD correct?
[no]: yes
Enter key password for <sslsocket>
(RETURN if same as keystore password):
Export certificate.cer
keytool -export -alias sslsocket -keystore sslkeystore.jks -rfc -file X509_certificate.cer
Enter keystore password:
Certificate stored in file <X509_certificate.cer>
Any suggestions on what is going wrong here?
What I am trying to achieve here is asymmetric encryption on a socket with public and private keys. Doesn't the *.jks hold the public key and the *.cer hold the private key? If this is the case do I need something additional in the client to use the private key or is it given to the client during the handshake?
Any help would be appreciated.
Upvotes: 3
Views: 8888
Reputation: 39029
You've got it backwards. As the Wikipedia article begins:
Public-key cryptography, or asymmetric cryptography, is any cryptographic system that uses pairs of keys: public keys that may be disseminated widely [which are mathematically] paired with private keys which are known only to the owner.
An SSL/TLS server keeps its private key private; it is the public key that is given out to others, specifically clients. But as wp also says
The binding between a public key and its "owner" must be correct, or else the algorithm may function perfectly and yet be entirely insecure in practice. [...] Associating a public key with its owner is typically done by protocols implementing a public key infrastructure – these allow the validity of the association to be formally verified by reference to a trusted third party in the form of either a hierarchical certificate authority (e.g., X.509) [...]
Although PKC in general has other options, SSL/TLS normally (and in Java always) uses X.509 certificates. A certificate for a party, here the server, contains the publickey of that party, the identity of that party (for SSL/TLS server, normally the DNS name or sometimes multiple names of the server), and some other information you can ignore, normally signed by a Certificate Authority.
Normally, reliers (clients) already have installed copies of the 'root' certs of well-known CAs like Verisign, GoDaddy, etc., and perhaps more local CAs like your employer or a state government, and the server simply sends its own certficate, plus any needed 'chain' or intermediate cert(s) provided by the CA, in the SSL/TLS handshake; the client can verify the cert using its pre-installed copy of the root. (For Java client, see below.)
However, if you don't bother to or aren't qualified to get a certificate from a real CA, the SSL/TLS protocol can instead use a certificate the server signs itself, called a self-signed certificate. In this case since the client has no a priori way of knowing that a particular cert is the correct one for the server and not a fake one created by an impostor, you need to put the server's self-signed cert in the client's truststore. In general you need to securely copy the cert from the server to the client(s), but since you are apparently testing on a single host that problem is greatly simplified.
Concretely in Java:
keytool -genkeypair
, or its obsolete synonym -genkey
, creates a keypair in a file, by default a JKS file, with a self-signed (default) certificate
keytool -exportcert
(or -export
) copies the certificate (containing the publickey and name) to a file, which here you named X509_certficate.cer
. Since you didn't get a real CA cert, this is the self-signed cert and you need to put it in the truststore of the/each client.
There are three ways of getting a cert into a Java client's truststore:
explicit 1: put the cert in a keystore file, usually JKS, using keytool -importcert
, read that keystore into memory, and use it to initalize a TrustManager
then an SSLContext
then an SSLSocketFactory
. This is similar to your server code except without the KeyManager
part and you use SSLSocket
classes instead of SSLServerSocket
classes.
explicit 2: read (just) the cert from a file using CertificateFactory
of type X.509
, create a keystore in memory (no keystore file) and put the cert in it, and then proceed as in explicit 1.
If you need multiple trusted certs, you can read each one and put them all in memory, but this quickly becomes more work than reading a single keystore file containing all of them.
middle: put the cert in a keystore file, and set system properties javax.net.ssl.trustStore
and ...trustStorePassword
(and ...trustStoreType
if not the default JKS, or in Java8 also PKCS12) to point to it, before the first creation of an SSL socket. You can set system properties by calling System.setProperty
, or using the flag -Dname=value
on the command line when starting java
.
implicit: add the cert to the file used by default, which is JREHOME/lib/security/cacerts
(or jssecacerts
if that exists, but by default it doesn't) and is installed to contain about a hundred well-known CAs. This affects all programs using that JRE unless they set their own truststores using the options above, which may or may not be an issue depending on what else you or anyone else runs (in Java) on this system.
Finally, CommonName in an SSL/TLS server certificate, which is the field actually set when keytool
prompts for first and last name
(notice CN=Gandalf
in the readback, CN means CommonName), should be the hostname of the server, or more exactly the name used by client(s) to connect to the server (which in your example was apparently localhost
). Alternatively, especially with real-CA certs, an extension named SubjectAlternativeName (abbreviated SAN) can be used for this, but doing SAN with keytool
is rather clumsy.
The basic SSLSocket
logic does NOT (currently) verify hostname, but most other clients like web browsers (and even HttpsURLConnection
in Java) do verify this, and unless your server can be connected as Gandalf
(without any domain qualification) those clients will refuse to connect to your server using the cert even though the cert itself is 'valid'.
Upvotes: 4