Q:

Resolve SSLHandshakeException certificate_unknown by using own keystore instead of adding certificate to Java JVM's default cacerts

This is my first time working with SSL, and I'm trying to create and use a self-signed certificate on my local machine (for now).
I've used the following batch file to create my certificate:
@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%

After that I've used the tutorial Secure Socket Connection Between a Client and a Server from Oracle, and downloaded those sample files as zip from here. Below the three files I use from this zip, slightly modified to use my own keystore with passphrase:
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) {}
    }
  }
}

I've created a jar file for both the ClassFileServer.jar and SSLSocketClientWithClientAuth.jar.
I first start the server-side with:
java -jar ClassFileServer.jar 2001 c:\ TLS true

And then the client-side with (the test.txt is a sample file I created to see if it can read this file and print its content):
java -jar SSLSocketClientWithClientAuth.jar 127.0.0.1 2001 C:\temp\localhost\test.txt

But I'm getting the following Exceptions:
Server-side output:
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)

Client-side output:
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

If I google the exception, I'm mainly getting solutions (like these or these) stating I should add the certificate to my Java JVM. I could do this by adding the following lines to the batch file that created the certificate:
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%

And then it indeed works. However, shouldn't this also work by using my own certificate keystore somehow, instead of the Java JVM's default cacerts one? I prefer to not have to add this certificate to the Java JVM cacert on every server I want to use it, especially when I'm adding this to our production code and start rolling out this update to customers in the future.
Any idea how I could modify the code so it won't give this error anymore, but I also won't have to add my certificate to the Java JVM?

A:

This error "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target" happens when the certification path is not found for a certificate. 
This is resolved by adding the certificate of the root Certification Authority in the trust store and including any intermediate CA's as well in the certificate itself. In your case, as you realized, it is resolved simply by adding your self-signed certificate in the cacerts.
You can configure your own cacert to be used by using the property javax.net.ssl.trustStore:
System.setProperty("javax.net.ssl.trustStore", path_to_your_jks_file);

