SSL套接字帮助 - javax.net.ssl.SSLHandshakeException:收到致命警报:certificate_unknown

时间:2016-10-13 18:09:06

标签: java sockets ssl ssl-certificate bouncycastle

我在使用SSL套接字建立客户端 - 服务器套接字连接时遇到问题。我仍处于SSL加密和处理证书和密钥库的学习过程中。我有一个客户端和服务器应用程序应该按如下方式相互连接:

SERVER - SSLReverseEchoer.java

.nos {
    position: relative;
    width: 12px; height: 12px;
    border: 2px solid #e04006;
    -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%;
    display: inline-block;
    color: #e04006;
    overflow: hidden;
    cursor: default;
}

.nos:hover {
    overflow: visible;
}

.nos span {
    position: absolute; top: -2px; left: -2px;
    height: 16px;
    padding-left: 18px;
    display: block;
    text-decoration: underline; font-size: 14px; line-height: 16px;
    white-space: nowrap;
}

.nos:before {
    content: '\21';
    position: absolute; top: -2px; left: -2px;
    width: 16px;
    display: block;
    font-family: Verdana, Geneva, sans-serif; text-align: center; font-weight: bold;  font-size: 13px; line-height: 13px;
}

客户端 - SSLSocketClient.java

import java.io.*;
import java.net.*;
import java.security.*;
import javax.net.ssl.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class SSLReverseEchoer {
  public static void main(String[] args) {
     String ksName = "sslkeystore.jks";
     String keystorePass = "sslkeystorepassword";
     char ksPass[] = keystorePass.toCharArray();

     int sslPort = 9099; 

     Security.addProvider(new BouncyCastleProvider());

     File file = new File(ksName);
     String absPath = file.getAbsolutePath(); 

     System.setProperty("javax.net.ssl.keyStore", absPath); 
     System.setProperty("javax.net.ssl.keyStorePassword", "sslkeystorepassword");

     try {

         //Get keystore w/ password
        KeyStore ks = KeyStore.getInstance("JKS");
        ks.load(new FileInputStream(ksName), ksPass);

        //Trust Manager
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        tmf.init(ks);

        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(ks, ksPass);

        SSLContext sc = SSLContext.getInstance("TLS");
        sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());

        SSLServerSocketFactory ssf = sc.getServerSocketFactory();
        SSLServerSocket s = (SSLServerSocket) ssf.createServerSocket(sslPort);
        printServerSocketInfo(s);

        //WAIT FOR CONNECTION TODO ADD THREAD for NIO
        SSLSocket c = (SSLSocket) s.accept();
        printSocketInfo(c);

        BufferedWriter w = new BufferedWriter(new OutputStreamWriter(
           c.getOutputStream()));
        BufferedReader r = new BufferedReader(new InputStreamReader(
           c.getInputStream()));

        String m = "Welcome to SSL Reverse Echo Server."+
           " Please type in some words.";
        w.write(m,0,m.length());
        w.newLine();
        w.flush();
        while ((m=r.readLine())!= null) {
           if (m.equals(".")) break;
           char[] a = m.toCharArray();
           int n = a.length;
           for (int i=0; i<n/2; i++) {
              char t = a[i];
              a[i] = a[n-1-i];
              a[n-i-1] = t;
           }
           w.write(a,0,n);
           w.newLine();
           w.flush();
        }
        w.close();
        r.close();
        c.close();
        s.close();
     } catch (Exception e) {
        e.printStackTrace();
     }
  }
  private static void printSocketInfo(SSLSocket s) {
     System.out.println("Socket class: "+s.getClass());
     System.out.println("   Remote address = "
        +s.getInetAddress().toString());
     System.out.println("   Remote port = "+s.getPort());
     System.out.println("   Local socket address = "
        +s.getLocalSocketAddress().toString());
     System.out.println("   Local address = "
        +s.getLocalAddress().toString());
     System.out.println("   Local port = "+s.getLocalPort());
     System.out.println("   Need client authentication = "
        +s.getNeedClientAuth());
     SSLSession ss = s.getSession();
     System.out.println("   Cipher suite = "+ss.getCipherSuite());
     System.out.println("   Protocol = "+ss.getProtocol());
  }
  private static void printServerSocketInfo(SSLServerSocket s) {
     System.out.println("Server socket class: "+s.getClass());
     System.out.println("   Socket address = "
        +s.getInetAddress().toString());
     System.out.println("   Socket port = "
        +s.getLocalPort());
     System.out.println("   Need client authentication = "
        +s.getNeedClientAuth());
     System.out.println("   Want client authentication = "
        +s.getWantClientAuth());
     System.out.println("   Use client mode = "
        +s.getUseClientMode());
  } 
}

我执行 SERVER 并开始侦听指定的端口,但是当执行 CLIENT 时,我得到以下堆栈跟踪。

服务器输出

import java.io.*;
import java.net.*;
import javax.net.ssl.*;

public class SSLSocketClient {

  public static void main(String[] args) {

     int sslPort = 9099; 

     BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
     PrintStream out = System.out;

     SSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault();

     try {
        SSLSocket c = (SSLSocket) f.createSocket("localhost", sslPort);
        printSocketInfo(c);
        c.startHandshake();

        BufferedWriter w = new BufferedWriter(new OutputStreamWriter(c.getOutputStream()));
        BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream()));

        String m = null;
        while ((m=r.readLine())!= null) {
           out.println(m);
           m = in.readLine();
           w.write(m,0,m.length());
           w.newLine();
           w.flush();
        }
        w.close();
        r.close();
        c.close();
     } catch (IOException e) {
        e.printStackTrace();
     }
  }
  private static void printSocketInfo(SSLSocket s) {
     System.out.println("Socket class: "+s.getClass());
     System.out.println("   Remote address = "
        +s.getInetAddress().toString());
     System.out.println("   Remote port = "+s.getPort());
     System.out.println("   Local socket address = "
        +s.getLocalSocketAddress().toString());
     System.out.println("   Local address = "
        +s.getLocalAddress().toString());
     System.out.println("   Local port = "+s.getLocalPort());
     System.out.println("   Need client authentication = "
        +s.getNeedClientAuth());
     SSLSession ss = s.getSession();
     System.out.println("   Cipher suite = "+ss.getCipherSuite());
     System.out.println("   Protocol = "+ss.getProtocol());
  }
}

客户输出

Server socket class: class sun.security.ssl.SSLServerSocketImpl
   Socket address = 0.0.0.0/0.0.0.0
   Socket port = 9099
   Need client authentication = false
   Want client authentication = false
   Use client mode = false
Socket class: class sun.security.ssl.SSLSocketImpl
   Remote address = /127.0.0.1
   Remote port = 62145
   Local socket address = /127.0.0.1:9099
   Local address = /127.0.0.1
   Local port = 9099
   Need client authentication = false
   Cipher suite = SSL_NULL_WITH_NULL_NULL
   Protocol = NONE
javax.net.ssl.SSLException: Connection has been shutdown: javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
    at sun.security.ssl.SSLSocketImpl.checkEOF(SSLSocketImpl.java:1541)
    at sun.security.ssl.SSLSocketImpl.checkWrite(SSLSocketImpl.java:1553)
    at sun.security.ssl.AppOutputStream.write(AppOutputStream.java:71)
    at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
    at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
    at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:295)
    at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:141)
    at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:229)
    at java.io.BufferedWriter.flush(BufferedWriter.java:254)
    at com.example.SSLReverseEchoer.main(SSLReverseEchoer.java:60)
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:154)
    at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:2023)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1125)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
    at sun.security.ssl.SSLSocketImpl.getSession(SSLSocketImpl.java:2267)
    at com.example.SSLReverseEchoer.printSocketInfo(SSLReverseEchoer.java:94)
    at com.example.SSLReverseEchoer.main(SSLReverseEchoer.java:49)

我使用keystore生成 sslkeystore.jks X509_certificate.cer ,其中包含以下参数:

生成sslkeystore.jks

Socket class: class sun.security.ssl.SSLSocketImpl
   Remote address = localhost/127.0.0.1
   Remote port = 9099
   Local socket address = /127.0.0.1:62145
   Local address = /127.0.0.1
   Local port = 62145
   Need client authentication = false
   Cipher suite = SSL_NULL_WITH_NULL_NULL
   Protocol = NONE
javax.net.ssl.SSLException: Connection has been shutdown: 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.SSLSocketImpl.checkEOF(SSLSocketImpl.java:1541)
    at sun.security.ssl.SSLSocketImpl.checkWrite(SSLSocketImpl.java:1553)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1399)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
    at com.example.SSLSocketClient.main(SSLSocketClient.java:23)
Caused by: 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(Alerts.java:192)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1509)
    at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216)
    at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979)
    at sun.security.ssl.Handshaker.process_record(Handshaker.java:914)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
    at sun.security.ssl.SSLSocketImpl.getSession(SSLSocketImpl.java:2267)
    at com.example.SSLSocketClient.printSocketInfo(SSLSocketClient.java:55)
    at com.example.SSLSocketClient.main(SSLSocketClient.java:22)
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(PKIXValidator.java:387)
    at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)
    at sun.security.validator.Validator.validate(Validator.java:260)
    at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324)
    at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229)
    at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1491)
    ... 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(SunCertPathBuilder.java:141)
    at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
    at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:382)
    ... 15 more

导出certificate.cer

keytool -genkey -keyalg RSA -alias sslsocket -keystore sslkeystore.jks -storepass sslkeystorepassword -validity 365 -keysize 2048
What is your first and last name?
  [Unknown]:  Gandalf
What is the name of your organizational unit?
  [Unknown]:  Wizardry
What is the name of your organization?
  [Unknown]:  Arnock  
What is the name of your City or Locality?
  [Unknown]:  Minas Tirith
What is the name of your State or Province?
  [Unknown]:  Gondor
What is the two-letter country code for this unit?
  [Unknown]:  GD
Is CN=Gandalf, OU=Wizardry, O=Arnock, L=Minas Tirith, ST=Gondor, C=GD correct?
  [no]:  yes

Enter key password for <sslsocket>
    (RETURN if same as keystore password):  

关于这里出了什么问题的任何建议?

我在这里想要实现的是使用公钥和私钥的套接字上的非对称加密。 * .jks不保存公钥,而* .cer保存私钥吗?如果是这种情况,我是否需要在客户端使用私钥或在握手期间将其提供给客户端?

任何帮助将不胜感激。

1 个答案:

答案 0 :(得分:2)

你已经倒退了。 As the Wikipedia article begins:

  

公钥加密非对称加密,是使用密钥对的任何加密系统:公钥,可能会广泛传播[在数学上]与私钥配对,只有所有者知道。

SSL / TLS服务器将其私钥保密;它是向其他人(特别是客户)发布的公钥。但正如wp所说

  

公钥与其所有者之间的绑定&#34;必须是正确的,否则算法可能完美地运作,但在实践中完全不安全。 [...]将公钥与其所有者相关联通常由实现公钥基础结构的协议来完成 - 这些允许通过以分层证书颁发机构的形式引用受信任的第三方来正式验证该关联的有效性。 (例如,X.509)[...]

尽管PKC通常还有其他选项,但SSL / TLS通常(并且总是在Java中)使用X.509证书。一方的证书,此处为服务器,包含该方的公钥,该方的身份(对于SSL / TLS服务器,通常为DNS名称或有时为多个您可以忽略的一些其他信息,通常由Certificate Authority 签名。

通常情况下,reliers(客户)已经安装了&#39; root&#39;的副本。知名CA的证书,如Verisign,GoDaddy等,以及可能更多本地CA,如您的雇主或州政府,服务器只需发送自己的证书,以及任何所需的链条&#39;在SSL / TLS握手中由CA提供的中间证书;客户端可以使用其预安装的根副本来验证证书。 (对于Java客户端,请参阅下文。)

但是,如果您没有资格或者没有资格从真实CA获得证书,则SSL / TLS协议可以而不是使用服务器签署的证书本身称为自签名证书。在这种情况下,由于客户端没有先验方式知道特定证书对于服务器是正确的,而不是由冒名顶替者创建的假证书,因此您需要将服务器放在服务器上。在客户的信任库中签名的自签名证书。通常,您需要将证书从服务器安全地复制到客户端,但由于您显然在单个主机上进行测试,因此问题会大大简化。

具体在Java中:

  • keytool -genkeypair或其过时的同义词-genkey在文件中创建密钥对,默认情况下为JKS文件,并带有自签名(默认)证书

  • keytool -exportcert(或-export)将证书(包含公钥和名称)复制到一个文件,在此您将其命名为X509_certficate.cer。由于您没有获得真正的CA证书,因此这是自签名证书,您需要将其放在/每个客户端的信任库中。

将证书存入Java客户端的信任库有三种方法:

  • 显式1:使用keytool -importcert将证书放入密钥库文件(通常为JKS)中,将该密钥库读入内存,并使用它来初始化TrustManager然后{{1}然后是SSLContext。这类似于您的服务器代码,除了没有SSLSocketFactory部分,您使用KeyManager类而不是SSLSocket类。

  • 显式2:使用类型为SSLServerSocket的{​​{1}}读取(仅)文件中的证书,在内存中创建密钥库(无密钥库文件)并将证书放入其中,并且然后按照明确的1进行。 如果您需要多个受信任的证书,您可以阅读每个证书并将它们全部放入内存中,但这很快就会比读取包含所有这些证书的单个密钥库文件更加有效。

  • middle:将cert放入密钥库文件中,并设置系统属性CertificateFactoryX.509(如果不是默认的JKS,则设置为javax.net.ssl.trustStore,或者在Java8中设置PKCS12)在第一次创建SSL套接字之前指向它。您可以在启动...trustStorePassword时通过调用...trustStoreType或在命令行上使用标志System.setProperty来设置系统属性。

  • 隐式:将证书添加到默认使用的文件中,即-Dname=value(如果存在,则为java,但默认情况下它不会安装到包含大约一百个着名的CA.这会影响使用该JRE的所有程序,除非他们使用上述选项设置自己的信任库,这可能是也可能不是问题,具体取决于您或其他任何人在此系统上运行的其他人(在Java中)。

最后,SSL / TLS服务器证书中的 CommonName ,这是JREHOME/lib/security/cacerts提示jssecacerts时实际设置的字段(在回读中注意keytool ,CN表示CommonName),应该是服务器的主机名,或者更确切地说是客户端用于连接的名称服务器(在您的示例中显然是first and last name)。或者,特别是对于真实CA证书,可以使用名为SubjectAlternativeName(缩写为SAN)的扩展名,但使用CN=Gandalf执行SAN非常笨拙。

基本localhost逻辑不会(当前)验证主机名,但大多数其他客户端(如Web浏览器(甚至是Java中的keytool)都会对此进行验证,除非您的服务器可以连接为{ {1}}(没有任何域名资格)这些客户端将拒绝使用证书连接到您的服务器,即使证书本身有效&#39;。