How to require client certificate with com.sun.net

2019-02-15 22:27发布

问题:

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

回答1:

In the end, there were several problems. - API permissions issue caused by Gradle setting an oddly qualified JDK - same JDK issue caused problems with SSLparams - My initial example didn't set the truststore.

Of interest, I ultimately changed to wantClientAuth(true), and authenticated in the handler.

package authserv;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.security.KeyStore;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
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);
    }
}

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.
        System.out.printf("Responding to host: %s\n",sess.getPeerHost());

        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();
    }
}

And here's a client to demonstrate both failure and success:

package authserv;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.net.SocketException;
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.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

public class AuthClient{
    static String NO_KEYSTORE = "";
    static String UNAUTH_KEYSTORE = "keys/unauthclient.jks"; // Doesn't exist in server trust store, should fail authentication.
    static String AUTH_KEYSTORE = "keys/authclient.jks"; // Exists in server trust store, should pass authentication.
    static String TRUSTSTORE = "keys/clienttrust.jks";
    static String CLIENT_PWD = "aaaaaa";

    public static void main(String[] args) throws Exception {

        AuthClient cl = new AuthClient();
        System.out.println("No keystore:");
        cl.testIt(NO_KEYSTORE);
        System.out.println("Unauth keystore:");
        cl.testIt(UNAUTH_KEYSTORE);
        System.out.println("Auth keystore:");
        cl.testIt(AUTH_KEYSTORE);
    }

    public void testIt(String jksFile){
        try {
            String https_url = "https://localhost:8888/auth/";
            URL url;
            url = new URL(https_url);
            HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
            conn.setSSLSocketFactory(getSSLFactory(jksFile));

            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 (SSLHandshakeException|SocketException e) {
            System.out.println(e.getMessage());
            System.out.println("");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static SSLSocketFactory getSSLFactory(String jksFile) throws Exception {
        // Create key store
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        KeyManager[] kmfs = null;
        if( jksFile.length() > 0 ) {
            keyStore.load(new FileInputStream(jksFile), 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();
    }
}


标签: java ssl https