SSLServerSocket и настройка сертификата - PullRequest
0 голосов
/ 15 ноября 2018

Я написал клиент-серверную Java-программу, используя ServerSocket и Socket объекты. Затем я изменил код для использования SSLServerSocket и 'SSLSocket`, однако я получаю разные исключения, в том числе:

javax.net.ssl.SSLHandshakeException: no cipher suites in common

Я надеюсь сделать столько программно, сколько смогу. Я также в порядке с самозаверяющими сертификатами.

В одном учебном пособии, за которым я следовал, предлагалось создать сертификат с помощью приложения java keytool, а затем переместить этот файл в ваш проект java. Я сделал это с помощью терминальной команды keytool -genkey -alias zastore -keyalg RSA -keystore za.store. Я назначил пароль для password.

Затем я вызываю функцию System.setProperty в надежде на работу SSLSockets, но она все еще не работает.

Вот мой код сервера

public class Server implements Runnable
{
    private SSLServerSocket serverSocket;
    private int portNumber;
    private Thread acceptThread;

    private LinkedList<Connection> connections;

    private ConnectionListener connectionListener;

    public Server(int port, ConnectionListener connectionListener)
    {
        this.connectionListener = connectionListener;
        portNumber = port;
        connections = new LinkedList<Connection>();
        try 
        {
            System.setProperty("javax.net.ssl.trustStore", "za.store");
            System.setProperty("javax.net.ssl.keyStorePassword", "password");
            SSLServerSocketFactory sslssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();

            serverSocket = (SSLServerSocket) sslssf.createServerSocket(portNumber,15);

        } 
        catch (IOException e) 
        {
            e.printStackTrace();
        }
    }

    public void startListening()
    {
        acceptThread = new Thread(this);
        acceptThread.start();
    }
    public void stopListening()
    {

        for(Connection c:connections)
        {
            c.stopListeningAndDisconnect();
        }

        try 
        {   
            serverSocket.close();
        } 
        catch (IOException e) 
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    @Override
    public void run() 
    {
        try 
        {
            while(true)
            {
                SSLSocket s = (SSLSocket) serverSocket.accept();
                Connection c = new Connection(s,connectionListener);
                connections.add(c);
                System.out.println("New Connection Established From"+s.getInetAddress().toString());
            }
        } 
        catch(java.net.SocketException e)
        {
            System.out.println("Listening thread terminated with exception.");
        }
        catch(IOException e) 
        {

            e.printStackTrace();
        }
    }

    public void removeConnection(Connection c)
    {
        connections.remove(c);
    }

    public void printConnections()
    {
        System.out.println("Number of connections "+connections.toString());

        for(int i=0; i<connections.size(); i++)
        {
            System.out.println(connections.toString());
        }
    }

}

А затем фрагмент моего клиентского кода, который подключается при нажатии кнопки:

@Override
    public void actionPerformed(ActionEvent e) 
    {
        if(e.getSource() == connect)
        {
            try 
            {
                System.setProperty("javax.net.ssl.trustStore", "za.store");
                System.setProperty("javax.net.ssl.keyStorePassword", "password");



                SSLSocketFactory sslsf = (SSLSocketFactory)SSLSocketFactory.getDefault();

                SSLSocket s = (SSLSocket)sslsf.createSocket(ipBox.getText(), Integer.parseInt(portBox.getText()));
                Connection c = new Connection(s,parent);
                parent.connectionSuccessful(c);
            } 
            catch (NumberFormatException e1) 
            {
                JOptionPane.showMessageDialog(this, "Error! Port number must be a number", "Error", JOptionPane.ERROR_MESSAGE);             
            } 
            catch (UnknownHostException e1) 
            {
                JOptionPane.showMessageDialog(this, "Error! Unable to find that host", "Error", JOptionPane.ERROR_MESSAGE);
            } 
            catch (IOException e1) 
            {


e1.printStackTrace();
        }   
    }
}

Одна статья Stackoverflow предполагает, что мой сервер "не имеет сертификата". Я не знаю, что это значит или как найти и найти его в нужном месте.

Ответы [ 2 ]

0 голосов
/ 17 ноября 2018

Вы неправильно устанавливаете свойства системы.

Серверу нужны javax.net.keyStore (не trustStore) и javax.net.keyStorePassword - и иногда javax.net.keyStoreType, но, вероятно, не в вашем случае. Для сервера, использующего самоподписанный сертификат, клиенту требуется хранилище доверенных сертификатов , чтобы содержать этот сертификат, и вообще не требуется хранилище ключей. Хотя это не задокументировано, если они находятся в той же системе, вы можете использовать хранилище ключей сервера (содержащее privateKeyEntry) в качестве хранилища доверенных сертификатов клиента, а JSSE автоматически обрабатывает листовой сертификат privateKey как доверенный сертификат. В любой другой ситуации вам нужно извлечь сертификат сервера (не ключ), скопировать / отправить его клиенту и использовать keytool -importcert, чтобы поместить сертификат (не ключ) в хранилище ключей файл , который используется как склад доверенных лиц; это может быть либо пользовательский файл (идентифицируемый с помощью sysprops), либо хранилище доверенных сертификатов по умолчанию в JRE / lib / security / cacerts (за исключением текущей Windows, если JRE меньше \Program Files[ (x86)], как по умолчанию для установщика, потому что теперь Windows портит людей, которые попробуй там поменять файлы).

И эти sysprops (если они используются) должны быть установлены ДО ЗАГРУЗКИ КЛАССОВ JSSE - устанавливать их при вызове SSL[Server]SocketFactory обычно слишком поздно. Обычно они устанавливаются в командной строке, которая вызывает Java с параметром -Dname=value; это гарантированно будет сделано достаточно рано. Если вы не можете этого сделать, поместите вызовы setProperty в начало вашего main метода или эквивалент для GUI.

Это означает как «отсутствие общего / общего шифра», поскольку без ключа + сертификата сервер не может поддерживать наборы шифров, которые выполняют проверку подлинности сервера, а Java / JSSE по умолчанию включает только наборы шифров, которые выполняют проверку подлинности сервера, поскольку другие небезопасны во многих / большинстве ситуаций.

Я уверен, что уже отвечал на вторую часть раньше, но не могу найти дурака. Первая часть встречалась во многих ответах, но обычно не четко представлена ​​в вопросе.

0 голосов
/ 15 ноября 2018

Следующая ошибка может возникать по разным причинам:

javax.net.ssl.SSLHandshakeException: no cipher suites in common

Точки, которые нужно проверить при отладке:

  1. Хранилище ключей и хранилище доверенных сертификатов правильно загружены и используются для созданиясоединения сокетов
  2. Сертификат совместим с включенными комплектами шифров
  3. На клиенте и сервере должен быть включен хотя бы один общий комплект шифров, и этот комплект шифров должен бытьтакже совместим с сертификатом

Например, в следующем примере я использую Java 8 с набором шифров по умолчанию.Сгенерированный мной сертификат использует ECDSA и SHA384, и, следовательно, когда между сервером и клиентом установлено соединение TLS, я вижу, что согласованный набор шифров равен TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, включив отладку (System.setProperty("javax.net.debug", "ssl");).

Ниже приведен рабочий пример:

В качестве первого шага необходимо создать пару ключей и сертификат.Для тестирования давайте создадим самозаверяющий сертификат и используем один и тот же сертификат как для сервера, так и для клиента:

keytool -genkeypair -alias server -keyalg EC \
-sigalg SHA384withECDSA -keysize 256 -keystore servercert.p12 \
-storetype pkcs12 -v -storepass abc123 -validity 10000 -ext san=ip:127.0.0.1

Давайте теперь создадим сервер:

package com.sapbasu.javastudy;

import java.io.InputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Objects;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManagerFactory;

/*
 * keytool -genkeypair -alias server -keyalg EC \
 * -sigalg SHA384withECDSA -keysize 256 -keystore servercert.p12 \
 * -storetype pkcs12 -v -storepass abc123 -validity 10000 -ext san=ip:127.0.0.1
 */

public class TLSServer {
  public void serve(int port, String tlsVersion, String trustStoreName,
      char[] trustStorePassword, String keyStoreName, char[] keyStorePassword)
      throws Exception {

    Objects.requireNonNull(tlsVersion, "TLS version is mandatory");

    if (port <= 0) {
      throw new IllegalArgumentException(
          "Port number cannot be less than or equal to 0");
    }

    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    InputStream tstore = TLSServer.class
        .getResourceAsStream("/" + trustStoreName);
    trustStore.load(tstore, trustStorePassword);
    tstore.close();
    TrustManagerFactory tmf = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(trustStore);

    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    InputStream kstore = TLSServer.class
        .getResourceAsStream("/" + keyStoreName);
    keyStore.load(kstore, keyStorePassword);
    KeyManagerFactory kmf = KeyManagerFactory
        .getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(keyStore, keyStorePassword);
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
        SecureRandom.getInstanceStrong());

    SSLServerSocketFactory factory = ctx.getServerSocketFactory();
    try (ServerSocket listener = factory.createServerSocket(port)) {
      SSLServerSocket sslListener = (SSLServerSocket) listener;

      sslListener.setNeedClientAuth(true);
      sslListener.setEnabledProtocols(new String[] {tlsVersion});
      // NIO to be implemented
      while (true) {
        try (Socket socket = sslListener.accept()) {
          PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
          out.println("Hello World!");
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
  }
}

Сейчассоздайте клиента:

package com.sapbasu.javastudy;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Objects;

import javax.net.SocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManagerFactory;

public class TLSClient {
  public String request(InetAddress serverHost, int serverPort,
      String tlsVersion, String trustStoreName, char[] trustStorePassword,
      String keyStoreName, char[] keyStorePassword) throws Exception {

    Objects.requireNonNull(tlsVersion, "TLS version is mandatory");

    Objects.requireNonNull(serverHost, "Server host cannot be null");

    if (serverPort <= 0) {
      throw new IllegalArgumentException(
          "Server port cannot be lesss than or equal to 0");
    }

    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    InputStream tstore = TLSClient.class
        .getResourceAsStream("/" + trustStoreName);
    trustStore.load(tstore, trustStorePassword);
    tstore.close();
    TrustManagerFactory tmf = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(trustStore);

    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    InputStream kstore = TLSClient.class
        .getResourceAsStream("/" + keyStoreName);
    keyStore.load(kstore, keyStorePassword);
    KeyManagerFactory kmf = KeyManagerFactory
        .getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(keyStore, keyStorePassword);
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
        SecureRandom.getInstanceStrong());

    SocketFactory factory = ctx.getSocketFactory();

    try (Socket connection = factory.createSocket(serverHost, serverPort)) {
      ((SSLSocket) connection).setEnabledProtocols(new String[] {tlsVersion});
      SSLParameters sslParams = new SSLParameters();
      sslParams.setEndpointIdentificationAlgorithm("HTTPS");
      ((SSLSocket) connection).setSSLParameters(sslParams);

      BufferedReader input = new BufferedReader(
          new InputStreamReader(connection.getInputStream()));
      return input.readLine();
    }
  }
}

Наконец, вот тест JUnit для проверки соединения:

package com.sapbasu.javastudy;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.InetAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.junit.jupiter.api.Test;

public class TLSServerClientTest {

  private static final int SERVER_PORT = 8444;
  private static final String TLS_VERSION = "TLSv1.2";
  private static final int SERVER_COUNT = 1;
  private static final String SERVER_HOST_NAME = "127.0.0.1";
  private static final String TRUST_STORE_NAME = "servercert.p12";
  private static final char[] TRUST_STORE_PWD = new char[] {'a', 'b', 'c', '1',
      '2', '3'};
  private static final String KEY_STORE_NAME = "servercert.p12";
  private static final char[] KEY_STORE_PWD = new char[] {'a', 'b', 'c', '1',
      '2', '3'};

  @Test
  public void whenClientSendsServerRequest_givenServerIsUp_returnsHelloWorld()
      throws Exception {
    TLSServer server = new TLSServer();
    TLSClient client = new TLSClient();

    System.setProperty("javax.net.debug", "ssl");

    ExecutorService serverExecutor = Executors.newFixedThreadPool(SERVER_COUNT);
    serverExecutor.submit(() -> {
      try {
        server.serve(SERVER_PORT, TLS_VERSION, TRUST_STORE_NAME,
            TRUST_STORE_PWD, KEY_STORE_NAME, KEY_STORE_PWD);
      } catch (Exception e) {
        e.printStackTrace();
      }
    });
    try {
      String returnedValue = client.request(
          InetAddress.getByName(SERVER_HOST_NAME), SERVER_PORT, TLS_VERSION,
          TRUST_STORE_NAME, TRUST_STORE_PWD, KEY_STORE_NAME, KEY_STORE_PWD);
      assertEquals("Hello World!", returnedValue);
    } catch (Exception e) {
      e.printStackTrace();
      throw e;
    }
  }
}

Примечание: сертификат (в данном примере servercert.p12) долженбыть в пути.В этом примере я сохранил его в папке test / resources структуры папок Maven, чтобы тест JUnit мог получить его в classpath.


Cipher Suite Background

При использовании TLS / SSL используемые криптографические алгоритмы определяются комплектами шифров.Сервер поддерживает набор наборов шифров (вы можете включать или отключать определенные наборы в соответствии с вашими потребностями и желаемым уровнем безопасности).Клиент также поддерживает наборы шифров.Во время настройки соединения используемый набор шифров согласовывается между клиентом и сервером.Предпочтение клиента будет учтено, учитывая, что сервер поддерживает этот конкретный набор шифров.

Вы можете найти список наборов шифров, поддерживаемых поставщиками Sun, до Java 8 здесь .

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

TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256

Здесь *

ECDHE стоитдля эллиптической кривой Diffie Hellman Ephemeral.Это алгоритм обмена ключами.Эллиптический вариант (первый E) используется для исполнения, тогда как эфемерный вариант (последний E) предназначен для прямой секретности.Прямая секретность означает, что если злоумышленник продолжает записывать все сообщения по TLS и в более поздний момент времени каким-то образом получает закрытый ключ, он / она не может расшифровать записанные в прошлом сообщения.

ECDSA - это алгоритм цифровой подписи, используемый для подписи ключа и используемый для аутентификации (проверки целостности) общего секрета.ECDSA слабее и медленнее, чем другие алгоритмы аутентификации, такие как HMAC.Тем не менее, он используется для аутентификации с общим ключом, потому что ему не нужно, чтобы верификатор знал секретный ключ, используемый для создания тега аутентификации.Сервер может очень хорошо использовать свой закрытый ключ для проверки целостности сообщения.

AES_128_GCM - После того, как общий секретный ключ открыт для обеих сторон (обычно это браузер и веб-сервер)), алгоритм шифрования симметричного блочного шифра используется для шифрования обмена сообщениями между сторонами.В данном конкретном случае используется блочный шифр AES со 128-битным ключом и режимом аутентификации GCM.

SHA256 - алгоритм хеширования

...