【问题标题】:Android app client Mutual TLS with java serverAndroid 应用程序客户端与 java 服务器的相互 TLS
【发布时间】:2019-10-17 18:38:53
【问题描述】:

我正在尝试使用双向 TLS 向我的服务器发送 https 请求。我成功使用 TLS 的服务器。但我不知道如何在客户端(Android 应用程序)上执行此操作。我在java服务器上使用spring。来自安卓应用的请求是使用HttpsUrlConnection() 发出的。

我成功地调用了HttpsUrlConnection(),这就是我的代码的样子:

public void test() {
        try {
            URL url = new URL(this.apiUrl);
            HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
            urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
            InputStream in = urlConnection.getInputStream();
            System.out.print(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

我的服务器配置为使用TLSv1.2 协议。 运行test() 会抛出这个错误:

W/System.err: javax.net.ssl.SSLHandshakeException: Handshake failed
        at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:288)
        at com.android.okhttp.internal.io.RealConnection.connectTls(RealConnection.java:196)
        at com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:153)
        at com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:116)
        at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:186)
        at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
        at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
        at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
        at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
W/System.err:     at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:411)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:248)
        at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getInputStream(DelegatingHttpsURLConnection.java:211)
W/System.err:     at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:30)
        at nl.management.finance.client.RaboClient.test(RaboClient.java:64)
        at nl.management.finance.MainActivity$RESTTask.doInBackground(MainActivity.java:31)
        at nl.management.finance.MainActivity$RESTTask.doInBackground(MainActivity.java:25)
        at android.os.AsyncTask$3.call(AsyncTask.java:378)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:289)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)
    Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x703daa2ff448: Failure in SSL library, usually a protocol error
    error:10000412:SSL routines:OPENSSL_internal:SSLV3_ALERT_BAD_CERTIFICATE (external/boringssl/src/ssl/tls_record.cc:587 0x703daa2b1148:0x00000001)
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
        at com.android.org.conscrypt.NativeSsl.doHandshake(NativeSsl.java:387)
        at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:226)
        ... 22 more

为什么我在堆栈跟踪中看到 SSLV3?不使用 TLSv1.2 吗? Wireshark 显示此https://ibb.co/27mpG4r

这段代码(来自@Hakan54)使SSLContext

public class SSLTrustManagerHelper {

    private InputStream keyStore;
    private String keyStorePassword;
    private InputStream trustStore;
    private String trustStorePassword;

    public SSLTrustManagerHelper(InputStream keyStore,
                                 String keyStorePassword,
                                 InputStream trustStore,
                                 String trustStorePassword) throws ClientException {
        if (keyStore == null || keyStorePassword.trim().isEmpty() || trustStore == null || trustStorePassword.trim().isEmpty()) {
            throw new ClientException("TrustStore or KeyStore details are empty, which are required to be present when SSL is enabled");
        }

        this.keyStore = keyStore;
        this.keyStorePassword = keyStorePassword;
        this.trustStore = trustStore;
        this.trustStorePassword = trustStorePassword;
    }

    public SSLContext clientSSLContext() throws ClientException {
        try {
            TrustManagerFactory trustManagerFactory = getTrustManagerFactory(trustStore, trustStorePassword);
            KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keyStore, keyStorePassword);
            this.keyStore.close();
            this.trustStore.close();

            return getSSLContext(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers());
        } catch (UnrecoverableKeyException | NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | KeyManagementException e) {
            e.printStackTrace();
            throw new ClientException(e);
        }
    }

    private static SSLContext getSSLContext(KeyManager[] keyManagers, TrustManager[] trustManagers) throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(keyManagers, trustManagers, null);
        return sslContext;
    }

    private static KeyManagerFactory getKeyManagerFactory(InputStream keystore, String keystorePassword) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, UnrecoverableKeyException, ClientException {
        KeyStore keyStore = loadKeyStore(keystore, keystorePassword);
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
        return keyManagerFactory;
    }

    private static TrustManagerFactory getTrustManagerFactory(InputStream truststore, String truststorePassword) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, ClientException {
        KeyStore trustStore = loadKeyStore(truststore, truststorePassword);
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);
        return trustManagerFactory;
    }

    private static KeyStore loadKeyStore(InputStream keystoreStream, String keystorePassword) throws ClientException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
        if (keystoreStream == null) {
            throw new ClientException("keystore was null.");
        }

        KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
        keystore.load(keystoreStream, keystorePassword.toCharArray());
        return keystore;
    }

}

【问题讨论】:

    标签: java android spring ssl


    【解决方案1】:

    您正在寻找的是基于证书的相互身份验证。服务器和客户端都需要相互信任才能进行通信。如果服务器只信任那个特定的客户端,那么任何其他客户端都不应该发出请求。

    上面的例子看起来还可以,但是用下面的例子配置起来会更容易:

    import static java.util.Objects.isNull;
    import static org.apache.commons.lang3.StringUtils.isBlank;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.security.KeyManagementException;
    import java.security.KeyStore;
    import java.security.KeyStoreException;
    import java.security.NoSuchAlgorithmException;
    import java.security.UnrecoverableKeyException;
    import java.security.cert.CertificateException;
    
    import javax.net.ssl.KeyManager;
    import javax.net.ssl.KeyManagerFactory;
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.TrustManager;
    import javax.net.ssl.TrustManagerFactory;
    
    public class SSLTrustManagerHelper {
    
        private String keyStore;
        private String keyStorePassword;
        private String trustStore;
        private String trustStorePassword;
    
        public SSLTrustManagerHelper(String keyStore,
                                     String keyStorePassword,
                                     String trustStore,
                                     String trustStorePassword) {
            if (isBlank(keyStore) || isBlank(keyStorePassword) || isBlank(trustStore) || isBlank(trustStorePassword)) {
                throw new ClientException("TrustStore or KeyStore details are empty, which are required to be present when SSL is enabled");
            }
    
            this.keyStore = keyStore;
            this.keyStorePassword = keyStorePassword;
            this.trustStore = trustStore;
            this.trustStorePassword = trustStorePassword;
        }
    
        public SSLContext clientSSLContext() {
            try {
                TrustManagerFactory trustManagerFactory = getTrustManagerFactory(trustStore, trustStorePassword);
                KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keyStore, keyStorePassword);
    
                return getSSLContext(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers());
            } catch (UnrecoverableKeyException | NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | KeyManagementException e) {
                throw new ClientException(e);
            }
        }
    
        private static SSLContext getSSLContext(KeyManager[] keyManagers, TrustManager[] trustManagers) throws NoSuchAlgorithmException, KeyManagementException {
            SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
            sslContext.init(keyManagers, trustManagers, null);
            return sslContext;
        }
    
        private static KeyManagerFactory getKeyManagerFactory(String keystorePath, String keystorePassword) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, UnrecoverableKeyException {
            KeyStore keyStore = loadKeyStore(keystorePath, keystorePassword);
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
            return keyManagerFactory;
        }
    
        private static TrustManagerFactory getTrustManagerFactory(String truststorePath, String truststorePassword) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
            KeyStore trustStore = loadKeyStore(truststorePath, truststorePassword);
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(trustStore);
            return trustManagerFactory;
        }
    
        private static KeyStore loadKeyStore(String keystorePath, String keystorePassword) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
            try(InputStream keystoreInputStream = SSLTrustManagerHelper.class.getClassLoader().getResourceAsStream(keystorePath)) {
                if (isNull(keystoreInputStream)) {
                    throw new ClientException(String.format("Could not find the keystore file with the given location %s", keystorePath));
                }
    
                KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
                keystore.load(keystoreInputStream, keystorePassword.toCharArray());
                return keystore;
            }
        }
    
    }
    

    您需要在此处提供密钥库和信任库的位置,以及密码。公共类将为您提供可以加载到 http 客户端的 ssl 上下文。

    确保您有一个包含私钥和公钥的客户端密钥库,以及一个您拥有服务器公钥的信任库。并确保服务器在其信任库中有客户端的公钥。您还需要在application.yml 文件中为您的服务器提供一个附加属性,该文件强制服务器验证客户端。属性为:client-auth: need

    在此处查看为服务器和客户端设置相互身份验证的完整示例,包括示例项目spring-boot-mutual-tls-sll

    2022 年更新

    我已经在一个库中提供了上述 sn-p 和其他实用程序,以使设置 ssl 配置更容易、更简洁。除此之外,它还包含一些验证。图书馆见这里GitHub - SSLContext Kickstart

    我最初提供的示例可以替换为:

    import javax.net.ssl.SSLContext;
    import javax.net.ssl.SSLSocketFactory;
    
    class App {
        public static void main(String[] args) {
            SSLFactory sslFactory = SSLFactory.builder()
                    .withIdentityMaterial("/path/to/resource/identity.jks", "password".toCharArray())
                    .withTrustMaterial("/path/to/resource/truststore.jks", "password".toCharArray())
                    .build();
    
            SSLContext sslContext = sslFactory.getSslContext();
            SSLSocketFactory sslSocketFactory = sslFactory.getSslSocketFactory();
        }
    }
    

    【讨论】:

    • 感谢您的回答。不过,这不适用于 android,因为您无法获得资源文件的路径。所以我在传递输入流的地方改变了它。我现在收到一个新错误。查看我更新的帖子。
    • 您的密钥库和信任库是两个独立的文件吗?或者您是否将您以这种方式创建的相同输入流:InputStream keystore = getResources().openRawResource(getResources().getIdentifier("keystore_v1", "raw", this.getPackageName())); 作为信任库参数传递给 SSLTrustManagerHelper 的构造函数并将其重用于密钥库参数?如果你两次使用相同的输入流,你会得到一个 EOFException
    • 您可以做两件事:1:从同一个文件创建两个输入流并将两者都传递给构造函数或 2:创建一个名为 truststore 的新密钥库,其中包含服务器的公钥。已经存在的密钥库应该只包含客户端的私钥和公钥。如果您不熟悉 keytool 的命令行工具,我建议您使用 KeyStore Explorer 或使用我编写的 cheatsheet 从命令行创建密钥库和其他内容
    • 您能否提供握手日志,因为当前的异常没有提供所有详细信息。当您提供以下 vm 参数时,您将在控制台中获得它:-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake
    • 服务器只要求客户端识别自己。如果在 SSL 握手期间存在此选项,则客户端将尝试发送客户端身份。可以通过从包含至少一个密钥对的密钥库创建密钥管理器来设置客户端身份。密钥对是具有公钥和私钥的对象。客户端主要将密钥库中的第一个条目发送给客户端。它将对公钥进行中心化。服务器将收到此信息并检查它是否信任它。如果此公钥(即证书)存在于信任库中,它将通过
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多