How can I have multiple SSL certificates for a Jav

2020-01-30 06:41发布

I have an in-house HTTP server written in Java; full source code at my disposal. The HTTP server can configure any number of web sites, each of which will have a separate listen socket created with:

skt=SSLServerSocketFactory.getDefault().createServerSocket(prt,bcklog,adr);

Using a standard key store created with the Java keytool, I cannot for the life of me work out how to get different certificates associated with different listen sockets so that each configured web site has it's own certificate.

I'm in a time pinch for this now, so some code samples that illustrate would be most appreciated. But as much I would appreciate any good overview on how JSSE hangs together in this regard (I have searched Sun's JSSE doco until my brain hurts (literally; though it might be as much caffeine withdrawal)).

Edit

Is there no simple way to use the alias to associate the server certificates in a key store with the listen sockets? So that:

  • The customer has one key store to manage for all certificates, and
  • There is no need to fiddle around with multiple key stores, etc.

I was getting the impression (earlier this afternoon) that I could write a simple KeyManager, with only chooseServerAlias(...) returning non-null, that being the name of the alias I wanted - anyone have any thoughts on that line of reasoning?

Solution

The solution I used, built from slyvarking's answer was to create a temporary key store and populate it with the desired key/cert extracted from the singular external key store. Code follows for any who are interested (svrctfals is my "server certificate alias" value):

    SSLServerSocketFactory              ssf;                                    // server socket factory
    SSLServerSocket                     skt;                                    // server socket

    // LOAD EXTERNAL KEY STORE
    KeyStore mstkst;
    try {
        String   kstfil=GlobalSettings.getString("javax.net.ssl.keyStore"        ,System.getProperty("javax.net.ssl.keyStore"        ,""));
        String   ksttyp=GlobalSettings.getString("javax.net.ssl.keyStoreType"    ,System.getProperty("javax.net.ssl.keyStoreType"    ,"jks"));
        char[]   kstpwd=GlobalSettings.getString("javax.net.ssl.keyStorePassword",System.getProperty("javax.net.ssl.keyStorePassword","")).toCharArray();

        mstkst=KeyStore.getInstance(ksttyp);
        mstkst.load(new FileInputStream(kstfil),kstpwd);
        }
    catch(java.security.GeneralSecurityException thr) {
        throw new IOException("Cannot load keystore ("+thr+")");
        }

    // CREATE EPHEMERAL KEYSTORE FOR THIS SOCKET USING DESIRED CERTIFICATE
    try {
        SSLContext        ctx=SSLContext.getInstance("TLS");
        KeyManagerFactory kmf=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        KeyStore          sktkst;
        char[]            blkpwd=new char[0];

        sktkst=KeyStore.getInstance("jks");
        sktkst.load(null,blkpwd);
        sktkst.setKeyEntry(svrctfals,mstkst.getKey(svrctfals,blkpwd),blkpwd,mstkst.getCertificateChain(svrctfals));
        kmf.init(sktkst,blkpwd);
        ctx.init(kmf.getKeyManagers(),null,null);
        ssf=ctx.getServerSocketFactory();
        }
    catch(java.security.GeneralSecurityException thr) {
        throw new IOException("Cannot create secure socket ("+thr+")");
        }

    // CREATE AND INITIALIZE SERVER SOCKET
    skt=(SSLServerSocket)ssf.createServerSocket(prt,bcklog,adr);
    ...
    return skt;

3条回答
够拽才男人
2楼-- · 2020-01-30 07:10

I recently ran into a similar situation. I have a custom embedded Java web server that can host any number of websites. Each website has its own domain name. Each website/domain is assigned a unique IP address on the server. A socket listener is created for each IP address on port 80.

For sites that have SSL certificates, I imported the keys and certificates into a single KeyStore. I assigned a certificate alias for each domain's SSL Certificates to match the domain name. Each domain/website that has an SSL Certificate is assigned a new socket listener on port 443.

By default, the standard Java X509KeyManager and the SunX509 implementation will pick the first aliases it finds for which there is a private key and a key of the right type for the chosen cipher suite (typically RSA). Unfortunately, the selected alias does not necessarily correspond to the requested domain so you end up with certificate errors.

To circumvent this issue, I used ZZ Coder's suggestion and implemented a custom X509KeyManager. Actually, for my server, I needed a X509ExtendedKeyManager which has an extra chooseEngineServerAlias() method.

My custom KeyManager relies on a hashmap of hostnames and their corresponding IP addresses. When a new SSL request is made, it checks the incoming IP address and finds the corresponding hostname. Then, it tries to find an alias in the keystore that corresponds to the hostname.

private class MyKeyManager extends X509ExtendedKeyManager implements X509KeyManager {
    private KeyStore keyStore;
    private char[] password;
    private java.util.HashMap<InetAddress, String> hosts;

    public MyKeyManager(KeyStore keystore, char[] password, java.util.HashMap<InetAddress, String> hosts) 
    throws IOException, GeneralSecurityException {
        this.keyStore = keystore;
        this.password = password;
        this.hosts = hosts;
    }

    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
        try{
            return hosts.get(InetAddress.getByName(engine.getPeerHost()));
        }
        catch(Exception e){
            return null;
        }
    }

    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
        return hosts.get(socket.getLocalAddress());
    }

    public PrivateKey getPrivateKey(String alias) {
        try {
            return (PrivateKey) keyStore.getKey(alias, password);
        } 
        catch (Exception e) {
            return null;
        }
    }

    public X509Certificate[] getCertificateChain(String alias) {
        try {
            java.security.cert.Certificate[] certs = keyStore.getCertificateChain(alias);
            if (certs == null || certs.length == 0) return null;
            X509Certificate[] x509 = new X509Certificate[certs.length];
            for (int i = 0; i < certs.length; i++){
                x509[i] = (X509Certificate)certs[i];
            }
            return x509;
        } 
        catch (Exception e) {
            e.printStackTrace();
            return null;
        }          
    }

    public String[] getServerAliases(String keyType, Principal[] issuers) {
        throw new UnsupportedOperationException("Method getServerAliases() not yet implemented.");
    }

    public String[] getClientAliases(String keyType, Principal[] issuers) {
        throw new UnsupportedOperationException("Method getClientAliases() not yet implemented.");
    }

    public String chooseClientAlias(String keyTypes[], Principal[] issuers, Socket socket) {
        throw new UnsupportedOperationException("Method chooseClientAlias() not yet implemented.");
    }

    public String chooseEngineClientAlias(String[] strings, Principal[] prncpls, SSLEngine ssle) {
        throw new UnsupportedOperationException("Method chooseEngineClientAlias() not yet implemented.");
    }        
}

The custom KeyManager is used initialize an SSLContext. The cool thing is that you only need to initialize one SSLContext.

javax.net.ssl.KeyManager[] kms = new javax.net.ssl.KeyManager[]{
     new MyKeyManager(keystore, keypass, hosts)
};
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(keystore);
javax.net.ssl.TrustManager[] tms = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kms, tms, null);
查看更多
干净又极端
3楼-- · 2020-01-30 07:18

The easiest way to do this is to use a single certificate for all your domain names. Put all other site names in SAN (Subject Alternative Name).

If you prefer one certificate for each domain name, you can write your own key manager and use alias to identify the domain so you can use a single keystore. In our system, we make a convention that keystore alias always equals the CN in the certificate. So we can do something like this,

SSLContext sctx1 = SSLContext.getInstance("SSLv3");
sctx1.init(new X509KeyManager[] { 
    new MyKeyManager("/config/master.jks","changeme".toCharArray(),"site1.example.com")
    },null, null);
SSLServerSocketFactory ssf = (SSLServerSocketFactory) sctx1.getServerSocketFactory();
ServerSocket ss1 = ssf.createServerSocket(1234);

...

SSLContext sctx2 = SSLContext.getInstance("SSLv3");
sctx2.init(new X509KeyManager[] { 
    new MyKeyManager("/config/master.jks","changeme".toCharArray(),"site2.example.com") 
    },null, null);
ssf = (SSLServerSocketFactory) sctx2.getServerSocketFactory();
ServerSocket ss2 = ssf.createServerSocket(5678);

...

public static class MyKeyManager implements X509KeyManager {
    private KeyStore keyStore;
    private String alias;
    private char[] password;

    MyKeyManager(String keyStoreFile, char[] password, String alias)
        throws IOException, GeneralSecurityException
    {
        this.alias = alias;
        this.password = password;
        InputStream stream = new FileInputStream(keyStoreFile);
        keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(stream, password);
    }

    public PrivateKey getPrivateKey(String alias) {
        try {
            return (PrivateKey) keyStore.getKey(alias, password);
        } catch (Exception e) {
            return null;
        }
    }

    public X509Certificate[] getCertificateChain(String alias) {
        try {
            java.security.cert.Certificate[] certs = keyStore.getCertificateChain(alias);
            if (certs == null || certs.length == 0)
                return null;
            X509Certificate[] x509 = new X509Certificate[certs.length];
            for (int i = 0; i < certs.length; i++)
                x509[i] = (X509Certificate)certs[i];
            return x509;
        } catch (Exception e) {
            return null;
        }          
    }

    public String chooseServerAlias(String keyType, Principal[] issuers,
                                    Socket socket) {
        return alias;
    }

    public String[] getClientAliases(String parm1, Principal[] parm2) {
        throw new UnsupportedOperationException("Method getClientAliases() not yet implemented.");
    }

    public String chooseClientAlias(String keyTypes[], Principal[] issuers, Socket socket) {
        throw new UnsupportedOperationException("Method chooseClientAlias() not yet implemented.");
    }

    public String[] getServerAliases(String parm1, Principal[] parm2) {
        return new String[] { alias };
    }

    public String chooseServerAlias(String parm1, Principal[] parm2) {
        return alias;
    }
}
查看更多
贼婆χ
4楼-- · 2020-01-30 07:22

You won't be able to use the default SSLServerSocketFactory.

Instead, initialize a different SSLContext for each site, each using a KeyManagerFactory configured with a key store containing a key entry with correct server certificate. (After initializing the KeyManagerFactory, pass its key managers to the init method of the SSLContext.)

After the SSLContext is initalized, get its SSLServerSocketFactory, and use that to create your listener.

KeyStore identity = KeyStore.getInstance(KeyStore.getDefaultType());
/* Load the keystore (a different one for each site). */
...
SSLContext ctx = SSLContext.getInstance("TLS");
KeyManagerFactory kmf = 
  KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(identity, password);
ctx.init(kmf.getKeyManagers(), null, null);
SSLServerSocketFactory factory = ctx.getServerSocketFactory();
ServerSocket server = factory.createSocket(port);
查看更多
登录 后发表回答