Устранить SSLHandshakeException certificate_unknown, используя собственное хранилище ключей вместо добавления сертификата в cacerts Java JVM по умолчанию. - PullRequest
2 голосов
/ 07 мая 2020

Я впервые работаю с SSL, и я пытаюсь создать и использовать самозаверяющий сертификат на моем локальном компьютере (пока).

Я использовал следующий командный файл для создать свой сертификат:

@ECHO off
rem keytool docs: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html

SET JAVA_HOME=C:\Program Files\Java\jre1.8.0_251
SET KEYTOOL="%JAVA_HOME%\bin\keytool.exe"
rem keystore name, which we'll set to the servername
SET KEYSTORE=127.0.0.1
SET EXPORT_ALIAS=localhost
SET SAN=127.0.0.1
rem CM=commonname, OU=organisation; O=province; C=country
SET DNAME="CN=localhost, OU=Kevin, O=Gelderland, C=NL"
SET CERT_PUB=localhost.crt
SET PASS=myPass

rem path the files will be output to:
cd c:\temp\localhost
c:

echo "create new keystore and self-signed certificate with corresponding public/private keys for the given alias: %EXPORT_ALIAS%"
%KEYTOOL% -genkeypair -alias %EXPORT_ALIAS% -keyalg RSA -keystore myKeystore.jks -validity 5000 -keysize 2048 -dname %DNAME% -keypass %PASS% -storepass %PASS% -ext san=dns:%SAN%

echo "reads from the newly created keystore for this alias %EXPORT_ALIAS%, and stores it as myKeystore.jks (in the certificate-file %CERT_PUB%)"
%KEYTOOL% -exportcert -rfc -alias %EXPORT_ALIAS% -keystore myKeystore.jks -file %CERT_PUB% -storepass %PASS%

echo "reads the newly created keystore myKeystore.jks (from certificate-file %CERT_PUB%), and stores it in the myTruststore.jks"
%KEYTOOL% -importcert -file %CERT_PUB% -alias %EXPORT_ALIAS% -keystore myTruststore.jks -storepass %PASS%

echo "creates a copy of the keystore myKeystore.jks to %KEYSTORE%"
%KEYTOOL% -importkeystore -srckeystore myKeystore.jks -destkeystore %KEYSTORE% -deststoretype PKCS12 -srcstorepass %PASS% -deststorepass %PASS%

После этого я использовал учебник Безопасное соединение сокета между клиентом и сервером из Oracle и загрузил эти образцы файлов в виде zip из здесь . Ниже приведены три файла, которые я использую из этого zip-архива, слегка измененные для использования моего собственного хранилища ключей с парольной фразой:

SSLSocketClientWithClientAuth. java class:

package client;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.security.KeyStore;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

/*
 * This example shows how to set up a key manager to do client
 * authentication if required by server.
 *
 * This program assumes that the client is not inside a firewall.
 * The application can be modified to connect to a server outside
 * the firewall by following SSLSocketClientWithTunneling.java.
 */
public class SSLSocketClientWithClientAuth {

  public static void main(final String[] args) throws Exception {
    String host = null;
    int port = -1;
    String path = null;
    for (final String arg : args) {
      System.out.println(arg);
    }

    if (args.length < 3) {
      System.out.println("USAGE: java SSLSocketClientWithClientAuth " +
        "host port requestedfilepath");
      System.exit(-1);
    }

    try {
      host = args[0];
      port = Integer.parseInt(args[1]);
      path = args[2];
    } catch (final IllegalArgumentException e) {
      System.out.println("USAGE: java SSLSocketClientWithClientAuth " +
        "host port requestedfilepath");
      System.exit(-1);
    }

    try {

      /*
       * Set up a key manager for client authentication
       * if asked by the server.  Use the implementation's
       * default TrustStore and secureRandom routines.
       */
      SSLSocketFactory factory = null;
      try {
        SSLContext ctx;
        KeyManagerFactory kmf;
        KeyStore ks;
        final char[] passphrase = "myPass".toCharArray();

        ctx = SSLContext.getInstance("TLS");
        kmf = KeyManagerFactory.getInstance("SunX509");
        ks = KeyStore.getInstance("JKS");

        ks.load(new FileInputStream("C:\\temp\\localhost\\myKeystore.jks"),
          passphrase);

        kmf.init(ks, passphrase);
        ctx.init(kmf.getKeyManagers(), null, null);

        factory = ctx.getSocketFactory();
      } catch (final Exception e) {
        throw new IOException(e.getMessage());
      }

      final SSLSocket socket = (SSLSocket) factory.createSocket(host, port);

      /*
       * send http request
       *
       * See SSLSocketClient.java for more information about why
       * there is a forced handshake here when using PrintWriters.
       */
      socket.startHandshake();

      final PrintWriter out = new PrintWriter(new BufferedWriter(
        new OutputStreamWriter(socket.getOutputStream())));
      out.println("GET " + path + " HTTP/1.0");
      out.println();
      out.flush();

      /*
       * Make sure there were no surprises
       */
      if (out.checkError()) {
        System.out.println("SSLSocketClient: java.io.PrintWriter error");
      }

      /* read response */
      final BufferedReader in = new BufferedReader(
        new InputStreamReader(socket.getInputStream()));

      String inputLine;

      while ((inputLine = in.readLine()) != null) {
        System.out.println(inputLine);
      }

      in.close();
      out.close();
      socket.close();

    } catch (final Exception e) {
      e.printStackTrace();
    }
  }
}

ClassFileServer. java class:

package server;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ServerSocket;
import java.security.KeyStore;

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

/* ClassFileServer.java -- a simple file server that can server
 * Http get request in both clear and secure channel
 *
 * The ClassFileServer implements a ClassServer that
 * reads files from the file system. See the
 * doc for the "Main" method for how to run this
 * server.
 */

public class ClassFileServer extends ClassServer {

  private static int DefaultServerPort = 2001;

  private static ServerSocketFactory getServerSocketFactory(final String type) {
    if (type.equals("TLS")) {
      SSLServerSocketFactory ssf = null;
      try {
        // set up key manager to do server authentication
        SSLContext ctx;
        KeyManagerFactory kmf;
        KeyStore ks;
        final char[] passphrase = "myPass".toCharArray();

        ctx = SSLContext.getInstance("TLS");
        kmf = KeyManagerFactory.getInstance("SunX509");
        ks = KeyStore.getInstance("JKS");

        ks.load(new FileInputStream("C:\\temp\\localhost\\myKeystore.jks"), passphrase);
        kmf.init(ks, passphrase);
        ctx.init(kmf.getKeyManagers(), null, null);

        ssf = ctx.getServerSocketFactory();
        return ssf;
      } catch (final Exception e) {
        e.printStackTrace();
      }
    } else {
      return ServerSocketFactory.getDefault();
    }
    return null;
  }

  /**
   * Main method to create the class server that reads
   * files. This takes two command line arguments, the
   * port on which the server accepts requests and the
   * root of the path. To start up the server: <br><br>
   *
   * <code>   java ClassFileServer <port> <path>
   * </code><br><br>
   *
   * <code>   new ClassFileServer(port, docroot);
   * </code>
   */
  public static void main(final String args[]) {
    System.out.println("USAGE: java ClassFileServer port docroot [TLS [true]]");
    System.out.println("");
    System.out.println("If the third argument is TLS, it will start as\n" +
      "a TLS/SSL file server, otherwise, it will be\n" +
      "an ordinary file server. \n" +
      "If the fourth argument is true,it will require\n" +
      "client authentication as well.");

    int port = DefaultServerPort;
    String docroot = "";

    if (args.length >= 1) {
      port = Integer.parseInt(args[0]);
    }

    if (args.length >= 2) {
      docroot = args[1];
    }
    String type = "PlainSocket";
    if (args.length >= 3) {
      type = args[2];
    }
    try {
      final ServerSocketFactory ssf = ClassFileServer.getServerSocketFactory(type);
      final ServerSocket ss = ssf.createServerSocket(port);
      if ((args.length >= 4) && args[3].equals("true")) {
        ((SSLServerSocket) ss).setNeedClientAuth(true);
      }
      new ClassFileServer(ss, docroot);
    } catch (final IOException e) {
      System.out.println("Unable to start ClassServer: " + e.getMessage());
      e.printStackTrace();
    }
  }

  private final String docroot;

  /**
   * Constructs a ClassFileServer.
   *
   * @param path the path where the server locates files
   */
  public ClassFileServer(final ServerSocket ss, final String docroot) throws IOException {
    super(ss);
    this.docroot = docroot;
  }

  /**
   * Returns an array of bytes containing the bytes for
   * the file represented by the argument <b>path</b>.
   *
   * @return the bytes for the file
   * @exception FileNotFoundException if the file corresponding
   * to <b>path</b> could not be loaded.
   */
  @Override
  public byte[] getBytes(final String path) throws IOException {
    System.out.println("reading: " + path);
    final File f = new File(this.docroot + File.separator + path);
    final int length = (int) (f.length());
    if (length == 0) {
      throw new IOException("File length is zero: " + path);
    } else {
      final FileInputStream fin = new FileInputStream(f);
      final DataInputStream in = new DataInputStream(fin);

      final byte[] bytecodes = new byte[length];
      in.readFully(bytecodes);
      return bytecodes;
    }
  }
}

ClassServer. java class:

package server;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/*
 * ClassServer.java -- a simple file server that can serve
 * Http get request in both clear and secure channel
 */

/**
 * Based on ClassServer.java in tutorial/rmi
 */
public abstract class ClassServer implements Runnable {

  /**
   * Returns the path to the file obtained from
   * parsing the HTML header.
   */
  private static String getPath(final BufferedReader in) throws IOException {
    String line = in.readLine();
    String path = "";
    // extract class from GET line
    if (line.startsWith("GET /")) {
      line = line.substring(5, line.length() - 1).trim();
      final int index = line.indexOf(' ');
      if (index != -1) {
        path = line.substring(0, index);
      }
    }

    // eat the rest of header
    do {
      line = in.readLine();
    } while ((line.length() != 0) && (line.charAt(0) != '\r') && (line.charAt(0) != '\n'));

    if (path.length() != 0) {
      return path;
    } else {
      throw new IOException("Malformed Header");
    }
  }

  private ServerSocket server = null;

  /**
   * Constructs a ClassServer based on <b>ss</b> and
   * obtains a file's bytecodes using the method <b>getBytes</b>.
   *
   */
  protected ClassServer(final ServerSocket ss) {
    this.server = ss;
    this.newListener();
  }

  /**
   * Returns an array of bytes containing the bytes for
   * the file represented by the argument <b>path</b>.
   *
   * @return the bytes for the file
   * @exception FileNotFoundException if the file corresponding
   * to <b>path</b> could not be loaded.
   * @exception IOException if error occurs reading the class
   */
  public abstract byte[] getBytes(String path) throws IOException, FileNotFoundException;

  /**
   * Create a new thread to listen.
   */
  private void newListener() {
    (new Thread(this)).start();
  }

  /**
   * The "listen" thread that accepts a connection to the
   * server, parses the header to obtain the file name
   * and sends back the bytes for the file (or error
   * if the file is not found or the response was malformed).
   */
  @Override
  public void run() {
    Socket socket;

    // accept a connection
    try {
      socket = this.server.accept();
    } catch (final IOException e) {
      System.out.println("Class Server died: " + e.getMessage());
      e.printStackTrace();
      return;
    }

    // create a new thread to accept the next connection
    this.newListener();

    try {
      final OutputStream rawOut = socket.getOutputStream();

      final PrintWriter out = new PrintWriter(new BufferedWriter(
        new OutputStreamWriter(rawOut)));
      try {
        // get path to class file from header
        final BufferedReader in = new BufferedReader(
          new InputStreamReader(socket.getInputStream()));
        final String path = getPath(in);
        // retrieve bytecodes
        final byte[] bytecodes = this.getBytes(path);
        // send bytecodes in response (assumes HTTP/1.0 or later)
        try {
          out.print("HTTP/1.0 200 OK\r\n");
          out.print("Content-Length: " + bytecodes.length + "\r\n");
          out.print("Content-Type: text/html\r\n\r\n");
          out.flush();
          rawOut.write(bytecodes);
          rawOut.flush();
        } catch (final IOException ie) {
          ie.printStackTrace();
          return;
        }

      } catch (final Exception e) {
        e.printStackTrace();
        // write out error response
        out.println("HTTP/1.0 400 " + e.getMessage() + "\r\n");
        out.println("Content-Type: text/html\r\n\r\n");
        out.flush();
      }

    } catch (final IOException ex) {
      // eat exception (could log error to log file, but
      // write out to stdout for now).
      System.out.println("error writing response: " + ex.getMessage());
      ex.printStackTrace();
    } finally {
      try {
        socket.close();
      } catch (final IOException e) {}
    }
  }
}

Я создал файл jar для ClassFileServer.jar и SSLSocketClientWithClientAuth.jar.

Сначала запустите серверную часть с:

java -jar ClassFileServer.jar 2001 c:\ TLS true

А затем клиентскую сторону с (test.txt - это образец файла, который я создал, чтобы увидеть, может ли он прочитать этот файл и распечатать его содержимое):

java -jar SSLSocketClientWithClientAuth.jar 127.0.0.1 2001 C:\temp\localhost\test.txt

Но я получаю следующие исключения:

Вывод на стороне сервера:

USAGE: java ClassFileServer port docroot [TLS [true]]

If the third argument is TLS, it will start as
a TLS/SSL file server, otherwise, it will be
an ordinary file server.
If the fourth argument is true,it will require
client authentication as well.
javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
        at sun.security.ssl.Alerts.getSSLException(Unknown Source)
        at sun.security.ssl.Alerts.getSSLException(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.recvAlert(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.readDataRecord(Unknown Source)
        at sun.security.ssl.AppInputStream.read(Unknown Source)
        at sun.nio.cs.StreamDecoder.readBytes(Unknown Source)
        at sun.nio.cs.StreamDecoder.implRead(Unknown Source)
        at sun.nio.cs.StreamDecoder.read(Unknown Source)
        at java.io.InputStreamReader.read(Unknown Source)
        at java.io.BufferedReader.fill(Unknown Source)
        at java.io.BufferedReader.readLine(Unknown Source)
        at java.io.BufferedReader.readLine(Unknown Source)
        at server.ClassServer.getPath(ClassServer.java:68)
        at server.ClassServer.run(ClassServer.java:156)
        at java.lang.Thread.run(Unknown Source)

Вывод на стороне клиента:

127.0.0.1
2001
C:\temp\localhost\test.txt
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.ssl.Alerts.getSSLException(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.fatal(Unknown Source)
        at sun.security.ssl.Handshaker.fatalSE(Unknown Source)
        at sun.security.ssl.Handshaker.fatalSE(Unknown Source)
        at sun.security.ssl.ClientHandshaker.serverCertificate(Unknown Source)
        at sun.security.ssl.ClientHandshaker.processMessage(Unknown Source)
        at sun.security.ssl.Handshaker.processLoop(Unknown Source)
        at sun.security.ssl.Handshaker.process_record(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
        at client.SSLSocketClientWithClientAuth.main(SSLSocketClientWithClientAuth.java:127)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.validator.PKIXValidator.doBuild(Unknown Source)
        at sun.security.validator.PKIXValidator.engineValidate(Unknown Source)
        at sun.security.validator.Validator.validate(Unknown Source)
        at sun.security.ssl.X509TrustManagerImpl.validate(Unknown Source)
        at sun.security.ssl.X509TrustManagerImpl.checkTrusted(Unknown Source)
        at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(Unknown Source)
        ... 9 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.provider.certpath.SunCertPathBuilder.build(Unknown Source)
        at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(Unknown Source)
        at java.security.cert.CertPathBuilder.build(Unknown Source)
        ... 15 more

Если я гуглил исключение, я в основном получаю решения (например, эти или эти ), в которых говорится, что я должен добавить сертификат в свою Java JVM. Я мог бы сделать это, добавив следующие строки в пакетный файл, создавший сертификат:

SET CACERT_PATH="%JAVA_HOME%\lib\security\cacerts"
SET CACERTS_PASS=changeit

...

echo "put the newly created myKeystore.jks (from certificate-file %CERT_PUB%) for this alias %EXPORT_ALIAS% also in the %CACERT_PATH% file for this server-side"
%KEYTOOL% -importcert -file %CERT_PUB% -keypass %PASS% -alias %EXPORT_ALIAS% -keystore %CACERT_PATH% -storepass %CACERTS_PASS%

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

Есть идеи, как я могу изменить код, чтобы он больше не выдавал эту ошибку, но мне также не нужно будет добавлять свой сертификат в Java JVM?

1 Ответ

2 голосов
/ 08 мая 2020

Эта ошибка «Не удалось построить путь PKIX: sun.security.provider.certpath.SunCertPathBuilderException: не удалось найти действительный путь сертификации для запрошенного целевого объекта» возникает, когда путь сертификации для сертификата не найден.

Это разрешается путем добавления сертификата центра сертификации root в хранилище доверенных сертификатов и включения любых промежуточных центров сертификации в сам сертификат. В вашем случае, как вы поняли, проблема решается простым добавлением вашего самозаверяющего сертификата в cacerts.

Вы можете настроить свой собственный cacert для использования с помощью свойства javax. net .ssl.trustStore :

System.setProperty("javax.net.ssl.trustStore", path_to_your_jks_file);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...