I want to require client certificate authentication with a server based on Java 1.7's built-in HttpsServer.
I cannot seem to find any way to make the server fail the authentication. It will merrily serve data to any old client at all, no matter whether its client cert is trusted, unknown, or entirely absent.
My read of the docs suggests that setting HttpsParameters.setNeedClientAuth(true) ought to cause an authentication failure when the client isn't trusted. I've found notes by folks with similar problems, variously suggesting the use of that same flag in SSLEngine and SSLParameters, but neither have changed the behaviour for me at all.
Here is the very simplest example I've been able to create. Looking at the meat of the transaction (with Wireshark or -Djavax.net.debug=all), I see nothing that looks obviously like a certificate request by the server... of course, that seems obvious, given that it's responding when it shouldn't.
I'm relatively new to both Java and SSL both. Am I misunderstanding the authentication process? Am I using in appropriate libraries? Am I overlooking a good way to troubleshoot this? Thanks!
Edit 1: Updated sample code to properly separate the client keystore and truststore. Also reworded question to make the authentication problem clearer.
package authserv;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URL;
import java.security.KeyStore;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsExchange;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;
public class AuthServer {
final static String SERVER_PWD = "aaaaaa";
final static String KST_SERVER = "keys/server.jks";
final static String TST_SERVER = "keys/servertrust.jks";
public static HttpsServer server;
public static void main(String[] args) throws Exception {
server = makeServer();
server.start();
//System.out.println("Server running, hit enter to stop.\n"); System.in.read();
AuthClient cl = new AuthClient();
cl.testIt();
server.stop(0);
}
public static HttpsServer makeServer() throws Exception {
server = HttpsServer.create(new InetSocketAddress(8888), 0);
//server.setHttpsConfigurator(new HttpsConfigurator(SSLContext.getInstance("TLS"))); // Default config with no auth requirement.
SSLContext sslCon = createSSLContext();
MyConfigger authconf = new MyConfigger(sslCon);
server.setHttpsConfigurator(authconf);
server.createContext("/auth", new HelloHandler());
return server;
}
private static SSLContext createSSLContext() {
SSLContext sslContext = null;
KeyStore ks;
KeyStore ts;
try{
sslContext = SSLContext.getInstance("TLS");
ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(KST_SERVER), SERVER_PWD.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, SERVER_PWD.toCharArray());
ts = KeyStore.getInstance("JKS");
ts.load(new FileInputStream(TST_SERVER), SERVER_PWD.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
} catch (Exception e) {
e.printStackTrace();
}
return sslContext;
}
}
class MyConfigger extends HttpsConfigurator {
public MyConfigger(SSLContext sslContext) {
super(sslContext); }
@Override
public void configure(HttpsParameters params) {
SSLContext sslContext = getSSLContext();
SSLParameters sslParams = sslContext.getDefaultSSLParameters();
sslParams.setNeedClientAuth(true);
params.setNeedClientAuth(true);
params.setSSLParameters(sslParams);
super.configure(params);
/* Other configure options that don't seem to help:
SSLEngine engine = sslContext.createSSLEngine ();
engine.setNeedClientAuth(true);
params.setCipherSuites ( engine.getEnabledCipherSuites () );
params.setProtocols ( engine.getEnabledProtocols () );
*/
}
}
class HelloHandler implements HttpHandler {
public void handle(HttpExchange t) throws IOException {
HttpsExchange ts = (HttpsExchange) t;
SSLSession sess = ts.getSSLSession();
//if( sess.getPeerPrincipal() != null) System.out.println(sess.getPeerPrincipal().toString()); // Principal never populated.
t.getResponseHeaders().set("Content-Type", "text/plain");
t.sendResponseHeaders(200,0);
String response = "Hello! You seem trustworthy!\n";
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
class AuthClient{
static String KEYSTORE = "";
static String TRUSTSTORE = "keys/clienttrust.jks";
static String CLIENT_PWD = "aaaaaa";
public static void main(String[] args) throws Exception {
KEYSTORE = "keys/unauthclient.jks"; // Doesn't exist in server trust store, should fail authentication.
//KEYSTORE = "keys/authclient.jks"; // Exists in server trust store, should pass authentication.
AuthClient cl = new AuthClient();
cl.testIt();
}
public void testIt(){
try {
String https_url = "https://localhost:8888/auth/";
URL url;
url = new URL(https_url);
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(getSSLFactory());
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setUseCaches(false);
// Print response
BufferedReader bir = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = null;
while((line = bir.readLine()) != null) {
System.out.println(line);
}
bir.close();
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
private static SSLSocketFactory getSSLFactory() throws Exception {
// Create key store
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
KeyManager[] kmfs = null;
if( KEYSTORE.length() > 0 ) {
keyStore.load(new FileInputStream(KEYSTORE), CLIENT_PWD.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, CLIENT_PWD.toCharArray());
kmfs = kmf.getKeyManagers();
}
// create trust store (validates the self-signed server!)
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(new FileInputStream(TRUSTSTORE), CLIENT_PWD.toCharArray());
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustFactory.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmfs, trustFactory.getTrustManagers(), null);
return sslContext.getSocketFactory();
}
}
Here's a bash script to create the necessary certificates and keystores.
#!/bin/bash
LOCALNAME=localhost
PASS=aaaaaa
function main
{
gen
list
}
function gen
{
mkdir -p keys
rm -f authclient.cert authclient.jks server.cert server.jks servertrust.jks clienttrust.jks unauthclient.jks
# create the keypairs for authclient, unauthclient and for server.
keytool -genkey -alias server -keyalg RSA -keystore server.jks -validity 365 -dname "cn=$LOCALNAME, ou=Auth, o=Auth, c=CA" -storepass $PASS -keypass $PASS
keytool -genkey -alias authclient -keyalg RSA -keystore authclient.jks -validity 365 -dname "cn=$LOCALNAME, ou=Auth, o=Auth, c=CA" -storepass $PASS -keypass $PASS
keytool -genkey -alias unauthclient -keyalg RSA -keystore unauthclient.jks -validity 365 -dname "cn=$LOCALNAME, ou=Auth, o=Auth, c=CA" -storepass $PASS -keypass $PASS
keytool -export -file server.cert -keystore server.jks -storepass $PASS -alias server
keytool -export -file authclient.cert -keystore authclient.jks -storepass $PASS -alias authclient
# Create a bare client truststore with no keypair
echo yes | keytool -import -file server.cert -alias server -keystore clienttrust.jks -storepass $PASS
# Create a truststore for the server containing ONLY authclient
echo yes | keytool -import -file authclient.cert -alias authclient -keystore servertrust.jks -storepass $PASS
# Add the server's cert to the client's keystores
#echo yes | keytool -import -file server.cert -alias server -keystore authclient.jks -storepass $PASS
#echo yes | keytool -import -file server.cert -alias server -keystore unauthclient.jks -storepass $PASS
}
function list {
for x in *.jks; do
SER=$(keytool -list -v -keystore $x -storepass aaaaaa | grep Serial)
echo $x $SER
done
}
main