java - 通过使用自己的 keystore 而不是将证书添加到 Java JVM 的默认 cacerts 来解决 SSLHandshakeExceptioncertificate_unknown

标签 java batch-file ssl keytool self-signed

这是我第一次使用 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%

之后我使用了教程 Secure Socket Connection Between a Client and a Server来自 Oracle,并从 here 下载这些示例文件作为 zip 。下面是我从该 zip 中使用的三个文件,稍加修改以使用我自己的 keystore 和密码:

SSLSocketClientWithClientAuth.java 类:

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

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

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) {}
    }
  }
}

我已经为 ClassFileServer.jarSSLSocketClientWithClientAuth.jar 创建了一个 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

如果我搜索异常,我主要得到解决方案(如 thesethese ),说明我应该将证书添加到我的 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%

然后它确实起作用了。但是,难道这不应该通过使用我自己的证书 keystore (而不是 Java JVM 的默认 cacerts keystore )来实现吗?我不想将此证书添加到我想要使用它的每台服务器上的 Java JVM cacert 中,尤其是当我将此证书添加到我们的生产代码中并开始向客户推出此更新时 future 。

知道如何修改代码,使其不再出现此错误,但我也不必将证书添加到 Java JVM 吗?

最佳答案

当找不到证书的证书路径时,会发生此错误“PKIX 路径构建失败:sun.security.provider.certpath.SunCertPathBuilderException:无法找到请求目标的有效证书路径”。

通过在信任存储中添加根证书颁发机构的证书并在证书本身中包含任何中间 CA 可以解决此问题。在您的情况下,正如您所意识到的,只需在 cacerts 中添加您的自签名证书即可解决此问题。

您可以使用属性javax.net.ssl.trustStore配置要使用的自己的cacert:

System.setProperty("javax.net.ssl.trustStore", path_to_your_jks_file);

关于java - 通过使用自己的 keystore 而不是将证书添加到 Java JVM 的默认 cacerts 来解决 SSLHandshakeExceptioncertificate_unknown,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61659375/

相关文章:

php - opencart 电子邮件无法正常工作

java - handshake.getPeerCertificates() 双向握手中的空指针

java - 正在用java开发API,所以我需要处理异常,我的customException类扩展了异常或运行时异常的类

batch-file - 使用批处理修改 .properties 文件中的字符串

java - 登录远程 Windows 服务器并执行 .bat 文件

windows-7 - 如何通过批处理脚本检查可用内存(RAM)?

java - Spring Data JPA 和事务管理

java - 验证该方法是否按预期工作的测试方法

java - 特定线程数

java - AXIS java 应用程序中的 SSLException : Unrecognized SSL message, 明文连接错误