Я впервые работаю с 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?