Android 客户端似乎没有发送证书(尝试相互身份验证)

标签 android ssl iot esp32 mbedtls

我正在尝试让一个 Android 应用程序对我自己的物联网服务器进行相互身份验证。
客户端似乎没有发送证书,也针对 https://server.cryptomix.com/secure 进行了验证.
简单的测试应用:

package info.backx.danny.alarm.jsontestapplication;

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HurlStack;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;
import com.google.android.material.snackbar.Snackbar;

import org.apache.commons.io.IOUtils;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;

public class FirstFragment extends Fragment {
    JsonTestApplication app;

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState
    ) {
        app = (JsonTestApplication) getActivity().getApplication();
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_first, container, false);
    }

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        view.findViewById(R.id.button_first).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                NavHostFragment.findNavController(FirstFragment.this)
                        .navigate(R.id.action_FirstFragment_to_SecondFragment);
            }
        });
        view.findViewById(R.id.hitme).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // DoRPC(view);
                DoRPC5(view);
            }
        });
    }

    final String myhostname = "lite2.dannybackx.dns-cloud.net";
//    final String rpc_url = "https://lite2.dannybackx.dns-cloud.net:4435/json";
//    final String rpc_url = "http://lite2.dannybackx.dns-cloud.net/json";
    final String http_url = "http://lite2.dannybackx.dns-cloud.net/json";

    final String test_json = "{ \"status\" : \"night\", \"name\" : \"hp\" }";

    final String rpc_url = "https://server.cryptomix.com/secure";
/*
 * Option 1 in https://stackoverflow.com/questions/59000913/mutual-authentication-using-retrofit-android
 *  but edited to use resource files
 *
 * I (09:45:07.098) esp_https_server: performing session handshake
 * E (09:45:09.093) esp-tls-mbedtls: mbedtls_ssl_handshake returned -29824 SSL - No client certification received from the client, but required by the authentication mode
 * E (09:45:09.098) esp_https_server: esp_tls_create_server_session failed
 * W (09:45:09.106) httpd: httpd_accept_conn: session creation failed
 * W (09:45:09.113) httpd: httpd_server: error accepting new connection
 *
 * E/DoRPC: DoRPC query {"status":"night","name":"hp"}, error java.net.SocketException: Connection reset
 * Disconnected from the target VM, address: 'localhost:38789', transport: 'socket'
 */

    public void DoRPC5(View view) {
        SSLSocketFactory socketFactory = null;
        try {
            char[] pass = "secret".toCharArray();
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");

            // InputStream trustedCertificateAsInputStream = Files.newInputStream(Paths.get("badssl-server-certificate.pem"), StandardOpenOption.READ);
            // Certificate trustedCertificate = certificateFactory.generateCertificate(trustedCertificateAsInputStream);
            Certificate trustedCertificate = certificateFactory.generateCertificate(
                    app.getResources().openRawResource(R.raw.chain));
            KeyStore trustStore = createEmptyKeyStore(pass);
            trustStore.setCertificateEntry("trust-server", trustedCertificate);

            //String pk = new String(Files.readAllBytes(Paths.get("private-key-badssl.pem")), Charset.defaultCharset());
            String pk = IOUtils.toString(app.getResources().openRawResource(R.raw.privkey));
            // IOUtils.closeQuietly(is); // don't forget to close your streams
            String privateKeyContent = pk
                    .replace("-----BEGIN PRIVATE KEY-----", "")
                    .replaceAll(System.lineSeparator(), "")
                    .replace("-----END PRIVATE KEY-----", "");

            byte[] privateKeyAsBytes = Base64.getDecoder().decode(privateKeyContent);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyAsBytes);

            // InputStream certificateChainAsInputStream = Files.newInputStream(Paths.get("certificate-chain-badssl.pem"), StandardOpenOption.READ);
            // Certificate certificateChain = certificateFactory.generateCertificate(certificateChainAsInputStream);
            Certificate certificateChain = certificateFactory.generateCertificate(
                    app.getResources().openRawResource(R.raw.fullchain));

            KeyStore identityStore = createEmptyKeyStore(pass);
            // identityStore.setKeyEntry("client", keyFactory.generatePrivate(keySpec), "".toCharArray(), new Certificate[]{certificateChain});
            identityStore.setKeyEntry("client", keyFactory.generatePrivate(keySpec),
                    "".toCharArray(), new Certificate[]{certificateChain});

            // trustedCertificateAsInputStream.close();
            // certificateChainAsInputStream.close();

            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(trustStore);
            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(identityStore, pass);
            KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();

            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(keyManagers, trustManagers, null);

            socketFactory = sslContext.getSocketFactory();

        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        RequestQueue queue = Volley.newRequestQueue(app,
                new HurlStack(null, socketFactory));

        try {
            JSONObject reqParam = new JSONObject(test_json);
            JsonObjectRequest jor = new JsonObjectRequest(Request.Method.PUT, rpc_url, reqParam,
                    new Response.Listener<JSONObject>() {
                        @Override
                        public void onResponse(JSONObject response) {
                            Snackbar.make(view, response.toString(), Snackbar.LENGTH_LONG)
                                    .setAction("Action", null).show();
                            Log.i("DoRPC", "Query " + reqParam.toString()
                                    + ", reply " + response.toString());
                        }
                    }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Snackbar.make(view, error.toString(), Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show();
                    Log.e("DoRPC", "DoRPC query " + reqParam.toString()
                            + ", error " + error.getLocalizedMessage());
                }
            });
            queue.add(jor);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        try {
            JSONObject reqParam = new JSONObject(test_json);
            StringRequest sr = new StringRequest(Request.Method.PUT, rpc_url, 
                    new Response.Listener<String>() {
                        @Override
                        public void onResponse(String response) {
                            Snackbar.make(view, response.toString(), Snackbar.LENGTH_LONG)
                                    .setAction("Action", null).show();
                            Log.i("DoRPC", "Query " + reqParam.toString()
                                    + ", reply " + response.toString());
                        }
                    }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Snackbar.make(view, error.toString(), Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show();
                    Log.e("DoRPC", "DoRPC query " + reqParam.toString()
                            + ", error " + error.getLocalizedMessage());
                }
            });
            queue.add(sr);
        } catch (JSONException e) {
            e.printStackTrace();
        }

    }

    public static KeyStore createEmptyKeyStore(char[] keyStorePassword) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, keyStorePassword);
        return keyStore;
    }

    /*
     * Authenticated query
     * https://medium.com/android-bits/android-security-tip-public-key-pinning-with-volley-library-fb85bf761857
     *
     * Not looked into yet :
     * https://stackoverflow.com/questions/39553999/how-can-i-make-android-volley-perform-https-request-using-a-certificate-self-si
     *
     * Note a way to test the server :
     * curl -X PUT --cert fullchain.pem --key privkey.pem https://lite2.dannybackx.dns-cloud.net:4435/json -d '{ "status" : "night", "name" : "hp"}'
     *
     * Note : this works but is not mutually authenticated.
     */
    public void DoRPC(View view) {
        RequestQueue queue = Volley.newRequestQueue(app,
                new HurlStack(null, getSocketFactory()));

        try {
            JSONObject reqParam = new JSONObject(test_json);
            JsonObjectRequest jor = new JsonObjectRequest(Request.Method.PUT, rpc_url, reqParam,
                    new Response.Listener<JSONObject>() {
                        @Override
                        public void onResponse(JSONObject response) {
                            Snackbar.make(view, response.toString(), Snackbar.LENGTH_LONG)
                                    .setAction("Action", null).show();
                            Log.i("DoRPC", "Query " + reqParam.toString()
                                    + ", reply " + response.toString());
                        }
                    }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Snackbar.make(view, error.toString(), Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show();
                    Log.e("DoRPC", "DoRPC query " + reqParam.toString()
                            + ", error " + error.getLocalizedMessage());
                }
            });
            queue.add(jor);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    private SSLSocketFactory getSocketFactory() {
        CertificateFactory cf = null;
        try {
            cf = CertificateFactory.getInstance("X.509");
            InputStream caInput = getResources().openRawResource(R.raw.mycert);
            Certificate ca;
            try {
                ca = cf.generateCertificate(caInput);
                Log.e("CERT", "ca=" + ((X509Certificate) ca).getSubjectDN());
            } finally {
                caInput.close();
            }

            String keyStoreType = KeyStore.getDefaultType();
            KeyStore keyStore = KeyStore.getInstance(keyStoreType);
            keyStore.load(null, null);
            keyStore.setCertificateEntry("ca", ca);

            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init(keyStore);

            HostnameVerifier hostnameVerifier = new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    Log.e("CipherUsed", session.getCipherSuite());
                    return hostname.compareTo(myhostname)==0; //The Hostname of your server.
                }
            };

            HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
            SSLContext context = SSLContext.getInstance("TLS");

            context.init(null, tmf.getTrustManagers(), null);
            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());

            SSLSocketFactory sf = context.getSocketFactory();
            return sf;
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }

        return  null;
    }
}
我错过了什么?
谢谢,
丹尼

最佳答案

找到了,identityStore.setKeyEntry()还应该作为密码传递(第三个参数),而不是空字符串。

关于Android 客户端似乎没有发送证书(尝试相互身份验证),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70471878/

相关文章:

azure - 是否可以将 MQTT 消息发送到事件中心?或者还有别的办法吗?

java - 微调器文本不显示默认值,也不显示所选值

ssl - 无法通过通用名称访问服务器

android - 如何使用 Fiddler4 从 Android App 捕获 HTTPS(TLS 1.0) 通信?

security - 在 TSL/SSL 握手中,如何使用 ClientHello 中发送的 Cipher Suite 信息?

android - 物联网通信 - REST API 与 Websockets

android - iOS 配置文件是否类似于 Android list 文件

java - 转换对象内的 Gson 数组

java - 如何修复第一次点击 'set' 为空,第二次点击仅获取日期

ios - MQTT-in-iOS - mqttDidDisconnect : Socket closed by remote peer