I'm trying to get an Android app to mutually authenticate to my own IoT server. The client doesn't seem to send a certificate, verified also against https://server.cryptomix.com/secure .
Simple test application:
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;
}
}
What am I missing ?
Thanks, Danny