TLS保护的TCP服务器和具有自签名证书的客户端

时间:2018-03-21 21:31:51

标签: android security ssl

我正在开发一款通过Wifi连接两台Android设备的应用,以便他们可以使用TCP交换文件/数据。自Android Oreo(API级别26)以来,最终有一个官方API:WifiManager.startLocalOnlyHotspot()。这将创建一个Wifi热点/网络,无需互联网访问。文档说:

  

应用程序还应该知道该网络将与其他应用程序共享。应用程序负责在此网络上保护其数据(例如, TLS )。

在通过TCP连接两台设备时,我没有使用TLS的经验,因此我搜索了一些提及自签名证书的方法。我不确定这是一个好习惯;无论如何,我无法让它发挥作用。任何帮助表示赞赏!

到目前为止我做了什么:

  1. 我使用OpenSSL创建了一个自签名证书,如answer所述:

    openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 10
    
  2. 我使用最新的(2018年3月)Bouncy Castle provider .jar file创建了一个新的密钥库,并添加了cert.pem。以下代码段源自this精彩博客文章,更具体地来自其sample app功能。

    ALIAS=`openssl x509 -inform PEM -subject_hash -noout -in cert.pem`
    
    keytool -import -v -trustcacerts \
            -alias $ALIAS \
            -file cert.pem \
            -keystore keystore_output_file \
            -storetype BKS \
            -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
            -providerpath bcprov-jdk15on-159.jar \
            -storepass my_keystore_password
    
  3. 我已将keystore_output_file添加到我的应用的src/res/raw/文件夹中,并初始化了SSLContext。然后我的应用在充当服务器时创建SSLServerSocket或在充当客户端时创建SSLSocket。最初我发现了这种方法here

    // Exception handling omitted
    KeyStore keyStore = KeyStore.getInstance("BKS");
    InputStream certStore = context.getResources().openRawResource(R.raw.keystore_output_file);
    
    keyStore.load(certStore, "my_keystore_password".toCharArray());
    
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init(keyStore);
    
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    keyManagerFactory.init(keyStore, "my_key_password".toCharArray());
    
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
    

    ...继续作为服务器

    SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
    SSLServerSocket sslServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(port);
    
    SSLSocket sslClientSocket = (SSLSocket) sslServerSocket.accept();
    

    ...继续作为客户

    SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
    SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(ipAddress, port);
    
    sslSocket.startHandshake();
    
  4. 问题:

    连接失败。根据服务器设备运行的Android版本,有不同的错误消息,但两者都提到了密码问题。我的测试设备的Android版本是4.3和7.1.1。堆栈跟踪是:

    服务器:Android 7.1.1

    javax.net.ssl.SSLHandshakeException: Handshake failed                                                                                 
        at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:429)                                         
        at com.android.org.conscrypt.OpenSSLSocketImpl.waitForHandshake(OpenSSLSocketImpl.java:682)                                       
        at com.android.org.conscrypt.OpenSSLSocketImpl.getInputStream(OpenSSLSocketImpl.java:644)                                         
        at com.candor.tlstcptest.ServerWorkerThread.run(ServerWorkerThread.java:46)                                  
    Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x8b0f8a40: Failure in SSL library, usually a protocol error
    error:100000b8:SSL routines:OPENSSL_internal:NO_SHARED_CIPHER (external/boringssl/src/ssl/s3_srvr.c:1059 0x99b4286a:0x00000000)       
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)                                                         
        at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357)      
    

    服务器:Android 4.3

    javax.net.ssl.SSLException: Could not find any key store entries to support the enabled cipher suites.
        at org.apache.harmony.xnet.provider.jsse.OpenSSLServerSocketImpl.checkEnabledCipherSuites(OpenSSLServerSocketImpl.java:232)
        at org.apache.harmony.xnet.provider.jsse.OpenSSLServerSocketImpl.accept(OpenSSLServerSocketImpl.java:177)
        at com.candor.tlstcptest.ServerThread.run(ServerThread.java:71)
    

    现在我卡住了,我甚至不知道如何开始解决这个问题,我还没有在网上找到有用的信息。我想知道是否真的没有关于这个主题的官方文档...正如我所说,任何帮助表示赞赏!感谢

1 个答案:

答案 0 :(得分:1)

这里的问题是您创建了一个仅包含证书的密钥库,而不是其私钥。 (这是keytool -import ...所做的。)

创建具有私钥条目(具有相应证书)的密钥库的一种方法是从OpenSSL创建PKCS#12存储,然后通过keytool将其转换为BKS(与{{或多或少相同的原则) 3}})。

openssl pkcs12 -export -in cert.pem -inkey key.pem -out store.p12

然后,使用keytool -importkeystore(不只是-import)将其转换为BKS。我没有尝试过确切的命令,但这应该是这样的:

keytool -importkeystore \
        -srckeystore store.p12 -srcstoretype PKCS12 \
        -destkeystore store.jks -deststoretype BKS \
        -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
        -providerpath bcprov-jdk15on-159.jar

(查看here以了解您可能还需要的确切选项。)

这应该导致store.jks密钥库也包含私钥。 该密钥库只应在服务器端使用,作为密钥库,而不是客户端的信任库。“(您已拥有的密钥库可用作keytool documentation,on客户方。)

几个旁注:

  • 使用SSL / TLS检查的另一件事是truststore(不只是相信证书是真品并由您认识的一方发行)。默认情况下,这并不总是在Java中检查(取决于您使用的选项)。
  • 对于这种特定类型的应用程序(主要是ad-hoc连接),您可能希望在安装时在服务器上生成证书/私钥对,向用户显示其指纹,然后拥有一个更宽松的信任管理器在显示指纹的客户端上,它必须以交互方式验证它(并且可能记住它)。

    如果您确实设法明确验证远程设备的单个证书,您可以安全地执行此操作而无需检查证书中的名称(只要客户端可以检查它是否连接到具有该确切名称的设备证书)。

    我对startLocalOnlyHotspot不够熟悉,但我怀疑其中许多细节将取决于热点使用的IP地址。我想它也嵌入了某种形式的DHCP服务器来给客户端一个IP地址,但我不确定客户端如何获取热点设备本身的IP地址(例如,可能有一些mDNS集成)