Как я могу получить несколько сертификатов SSL для сервера Java

У меня есть собственный HTTP-сервер, написанный на Java; полный исходный код в моем распоряжении. HTTP-сервер может настраивать любое количество веб-сайтов, каждый из которых будет иметь отдельный сокет для прослушивания, созданный с помощью

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

Используя стандартный хранилище ключей, созданный с помощью Java keytool, я не могу в течение всей жизни выяснить, как получить разные сертификаты, связанные с различными сокетами для прослушивания, чтобы каждый настроенный веб-сайт имел свой собственный сертификат.

Я сейчас нахожусь в таком положении, поэтому некоторые примеры кода, которые иллюстрируют, будут наиболее оценены. Но я бы очень признателен за хороший обзор того, как JSSE висит вместе в этом отношении (я искал Sun JSSE doco до тех пор, пока мой мозг не повредит (в буквальном смысле, хотя это может быть как отказ от кофеина)).

Edit

Нет ли простого способа использовать псевдоним для связывания сертификатов сервера в хранилище ключей с помощью сокетов прослушивания? Так что:

  • Клиент имеет один хранилище ключей для управления всеми сертификатами и
  • Нет необходимости возиться с несколькими хранилищами ключей и т.д.

У меня создалось впечатление (ранее сегодня днем), что я могу написать простой KeyManager, причем только chooseServerAlias(...) возвращает ненулевое значение, являющееся именем псевдонима, который я хотел - у кого-то есть мысли по этой линии рассуждений

Решение

Решением, которое я использовал, построенным из 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;

Ответы

Ответ 1

Самый простой способ сделать это - использовать один сертификат для всех ваших доменных имен. Поместите все другие имена сайтов в SAN (альтернативное имя темы).

Если вы предпочитаете один сертификат для каждого имени домена, вы можете написать свой собственный менеджер ключей и использовать псевдоним для идентификации домена, чтобы вы могли использовать одно хранилище ключей. В нашей системе мы делаем соглашение о том, что псевдоним хранилища ключей всегда равен 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;
    }
}

Ответ 2

Вы не сможете использовать по умолчанию SSLServerSocketFactory.

Вместо инициализировать другой SSLContext для каждого сайта, каждый из которых использует KeyManagerFactory настроен с хранилищем ключей, содержащим ключевую запись с правильным сертификатом сервера. (После инициализации KeyManagerFactory передайте менеджерам ключей методу init SSLContext.)

После инициализации SSLContext получите 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);

Ответ 3

Недавно я столкнулся с подобной ситуацией. У меня есть собственный встроенный веб-сервер Java, который может размещать любое количество веб-сайтов. На каждом веб-сайте есть собственное доменное имя. Каждому веб-сайту/домену присваивается уникальный IP-адрес на сервере. Для каждого IP-адреса на порту 80 создается прослушиватель сокетов.

Для сайтов, имеющих SSL-сертификаты, я импортировал ключи и сертификаты в один KeyStore. Я назначил псевдоним сертификата для каждого сертификата SSL домена, чтобы он соответствовал доменному имени. Каждому домену/веб-сайту, имеющему сертификат SSL, назначается новый прослушиватель сокетов на порту 443.

По умолчанию стандартный Java X509KeyManager и реализация SunX509 выберет первые псевдонимы, которые он найдет, для которых есть закрытый ключ и ключ нужного типа для выбранного набора шифров (обычно RSA). К сожалению, выбранный псевдоним не обязательно соответствует запрашиваемому домену, поэтому вы получаете ошибки сертификата.

Чтобы обойти эту проблему, я использовал предложение ZZ Coder и внедрил пользовательский X509KeyManager. На самом деле для моего сервера мне нужен X509ExtendedKeyManager, у которого есть дополнительный метод selectEngineServerAlias ​​().

Мой пользовательский 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.");
    }        
}

Пользовательский 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);