如何为 Java 服务器拥有多个 SSL 证书

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1788031/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-12 22:39:30  来源:igfitidea点击:

How can I have multiple SSL certificates for a Java server

javasslcertificate

提问by Lawrence Dol

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:

我有一个用 Java 编写的内部 HTTP 服务器;我可以使用完整的源代码。HTTP 服务器可以配置任意数量的网站,每个网站都有一个单独的侦听套接字,使用以下内容创建:

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.

使用使用 Java keytool 创建的标准密钥库,我终生无法弄清楚如何获取与不同侦听套接字关联的不同证书,以便每个配置的网站都有自己的证书。

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)).

我现在很忙,所以一些说明的代码示例将不胜感激。但是,我非常感谢任何关于 JSSE 如何在这方面结合在一起的良好概述(我已经搜索了 Sun 的 JSSE doco,直到我的大脑受伤(字面意思;尽管它可能与咖啡因戒断一样多))。

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?

我得到的印象是(今天下午早些时候)我可以编写一个简单的 KeyManager,只chooseServerAlias(...)返回非空值,这是我想要的别名的名称 - 有没有人对这条推理有任何想法?

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):

我使用的解决方案是根据slyvarking的答案构建的,是创建一个临时密钥存储并使用从单一外部密钥存储中提取的所需密钥/证书填充它。以下代码供任何感兴趣的人使用(svrctfals 是我的“服务器证书别名”值):

    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;

采纳答案by ZZ Coder

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).

最简单的方法是为您的所有域名使用一个证书。将所有其他站点名称放在 SAN(主题备用名称)中。

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,

如果您更喜欢每个域名一个证书,您可以编写自己的密钥管理器并使用别名来标识域,以便您可以使用单个密钥库。在我们的系统中,我们约定密钥库别名始终等于证书中的 CN。所以我们可以做这样的事情,

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

回答by erickson

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

您将无法使用默认的SSLServerSocketFactory.

Instead, initializea different SSLContextfor each site, each using a KeyManagerFactoryconfiguredwith a key store containing a key entry with correct server certificate. (After initializing the KeyManagerFactory, pass its key managersto the initmethod of the SSLContext.)

相反,为每个站点初始化一个不同SSLContext的站点,每个站点都使用一个KeyManagerFactory配置的密钥库,其中包含一个带有正确服务器证书的密钥条目。(初始化 后KeyManagerFactory,将其密钥管理器传递给 的init方法SSLContext。)

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

在之后SSLContext的initalized,得到它SSLServerSocketFactory,并用它来创建你的听众。

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

回答by Peter

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.

我最近遇到了类似的情况。我有一个自定义的嵌入式 Java Web 服务器,可以托管任意数量的网站。每个网站都有自己的域名。每个网站/域都在服务器上分配了一个唯一的 IP 地址。为端口 80 上的每个 IP 地址创建一个套接字侦听器。

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.

对于具有 SSL 证书的站点,我将密钥和证书导入到单个 KeyStore 中。我为每个域的 SSL 证书分配了一个证书别名以匹配域名。每个拥有 SSL 证书的域/网站都会在端口 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.

默认情况下,标准的 Java X509KeyManager 和 SunX509 实现将选择它找到的第一个别名,其中有一个私钥和一个用于所选密码套件(通常是 RSA)的正确类型的密钥。不幸的是,所选别名不一定对应于请求的域,因此您最终会遇到证书错误。

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

为了规避这个问题,我使用了ZZ Coder 的建议并实现了一个自定义的 X509KeyManager。实际上,对于我的服务器,我需要一个 X509ExtendedKeyManager,它有一个额外的 chooseEngineServerAlias() 方法。

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.

我的自定义 KeyManager 依赖于主机名及其相应 IP 地址的哈希图。当发出新的 SSL 请求时,它会检查传入的 IP 地址并找到相应的主机名。然后,它尝试在与主机名对应的密钥库中查找别名。

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.

自定义 KeyManager 用于初始化 SSLContext。很酷的一点是你只需要初始化一个 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);

UPDATE

更新

I ran into a situation where the engine.getPeerHost()wasn't working as expected so I had to refactor the chooseEngineServerAlias() method to rely on SNI instead.

我遇到了engine.getPeerHost()无法按预期工作的情况,因此我不得不重构chooseEngineServerAlias() 方法以改为依赖 SNI。

public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
    if (alias!=null) return alias;
    else{
        try{

          //Get hostname from SSL handshake
            ExtendedSSLSession session = (ExtendedSSLSession) engine.getHandshakeSession();
            String hostname = null;
            for (SNIServerName name : session.getRequestedServerNames()) {
                if (name.getType() == StandardConstants.SNI_HOST_NAME) {
                    hostname = ((SNIHostName) name).getAsciiName();
                    break;
                }
            }


            String[] arr = hostname.split("\.");
            hostname = arr[arr.length-2] + "." + arr[arr.length-1];
            return aliases.get(InetAddress.getByName(hostname));
        }
        catch(Exception e){
            return null;
        }
    }
}