How can I make Android Volley perform HTTPS request, using a certificate self-signed by an Unknown CA?
Asked Answered
Y

3

19

Before making the question, I found some links, which I checked, one by one, and none of them, gives me a solution:

The only link which I have found until now, is this one, which gives two approaches: Making a HTTPS request using Android Volley

  • 1º Instructs to import some classes to your app, when indeed, there are another classes that must be imported, and the classes are using deprecated libs from "apache.org"
  • 2º A example to NUKE all SSL ceriticates (pretty bad idea...)


I also have found this blog, which has a plenty of explanations, but at the end, I realized that the examples are using deprecated libraries from "apache.org" and also, the blog itself doesn't have a content for Android Volley. https://nelenkov.blogspot.mx/2011/12/using-custom-certificate-trust-store-on.html


There is also this link from Android and the code of "Unknown certificate authority" section, which gives a good idea about the solution, but the code itself, lacks something in its structure (Android Studio complaining...): https://developer.android.com/training/articles/security-ssl.html

But this quote from the link, seems the core concept for solving the problem.

"A TrustManager is what the system uses to validate certificates from the server and—by creating one from a KeyStore with one or more CAs—those will be the only CAs trusted by that TrustManager. Given the new TrustManager, the example initializes a new SSLContext which provides an SSLSocketFactory you can use to override the default SSLSocketFactory from HttpsURLConnection. This way the connection will use your CAs for certificate validation."



And now, here is my problem: I have a webserver that is using a self-signed certificate and I have created a "BKS truststore" based on its certificate. I have imported de BKS truststore to my Android APP and now, I have the following code on my App (I'm just posting here the MainActivity, which is the only class that has relevance to this subject until now, I suppose):

package com.domain.myapp;


import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;

import android.widget.EditText;
import android.widget.Toast;

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.StringRequest;
import com.android.volley.toolbox.Volley;

import java.io.InputStream;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;


public class LoginScreen extends AppCompatActivity {

Context ctx          = null;
InputStream inStream = null;
HurlStack hurlStack  = null;

EditText username    = null;
EditText password    = null;
String loginStatus   = null;

public LoginScreen() {

    try {
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        KeyStore ks = KeyStore.getInstance("BKS");
        inStream = ctx.getApplicationContext().getResources().openRawResource(R.raw.mytruststore);
        ks.load(inStream, null);
        inStream.close();
        tmf.init(ks);
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), null);
        final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        hurlStack = new HurlStack(null, sslSocketFactory);
    } catch (Exception e){
        Log.d("Exception:",e.toString());
    }
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login_screen);
    username = (EditText) findViewById(R.id.user);
    password = (EditText) findViewById(R.id.passwd);
}

public void login(View view) {

    RequestQueue queue = Volley.newRequestQueue(this, hurlStack);
    final String url = "https://myserver.domain.com/app/login";

    StringRequest postRequest = new StringRequest(Request.Method.POST, url,
            new Response.Listener<String>()
            {
                @Override
                public void onResponse(String response) {
                    Log.d("Response", response);
                    loginStatus = "OK";
                }
            },
            new Response.ErrorListener()
            {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.d("Error.Response", String.valueOf(error));
                    loginStatus = "NOK";
                }
            }
    ) {
        @Override
        protected Map<String, String> getParams()
        {
            Map<String, String>  params = new HashMap<String, String>();
            params.put("username", String.valueOf(user));
            params.put("domain", String.valueOf(passwd));

            return params;
        }
    };
    queue.add(postRequest);

    if (loginStatus == "OK") {
        Intent intent = new Intent(LoginScreen.this, OptionScreen.class);
        startActivity(intent);
    } else {
        Toast.makeText(getApplicationContext(), "Login failed",Toast.LENGTH_SHORT).show();
    }
}

}

Regarding the constructor class, I took the liberty of copying the code, putting some comments about what do I understand from each part of it:

try {
// I have a TrustManagerFactory object
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// I have a KeyStore considering BKS (BOUNCY CASTLE) KeyStore object
KeyStore ks = KeyStore.getInstance("BKS");
// I have configured a inputStream using my TrustStore file as a Raw Resource
inStream = ctx.getApplicationContext().getResources().openRawResource(R.raw.mytruststore);
// I have loaded my Raw Resource into the KeyStore object
ks.load(inStream, null);
inStream.close();
// I have initialiazed my Trust Manager Factory, using my Key Store Object
tmf.init(ks);
// I have created a new SSL Context object
SSLContext sslContext = SSLContext.getInstance("TLS");
// I have initialized my new SSL Context, with the configured Trust Managers found on my Trust Store
sslContext.init(null, tmf.getTrustManagers(), null);
// I have configured a HttpClientStack, using my brand new Socket Context
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
hurlStack = new HurlStack(null, sslSocketFactory);
} catch (Exception e){
Log.d("Exception:",e.toString());
}

After this, in another Class Method, I have the RequestQueue, using the HttpClientStack which I have configured on the Class COnstructor:

RequestQueue queue = Volley.newRequestQueue(this, hurlStack);
final String url = "https://myserver.domain.com/app/login";
StringRequest postRequest = new StringRequest(Request.Method.POST, url,new Response.Listener<String>()
    {
    ...
    ...
    }

When I run my app, giving the user and password which is expected by my WebServer, I can see in the Android Monitor from Android Studio the following messages:

09-17 21:57:13.842 20617-20617/com.domain.myapp D/Error.Response: com.android.volley.NoConnectionError: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

After all this explanation, I have the following question:

  • What else must be configured in order to make Android accept the SSL certificate from the CA of the custom TrustManager which I have configured at the constructor of the class?

Forgive me, but I'm beginner on Android programming, as well on Java, so maybe, I'm making a terrible mistake...

Any help, would be much appreciated.

UPDATE

I have improved the constructor of the class, doing a better grouping of the statements, and also using the KeyManagerFactory, which seems to be pretty important on this process. Here goes:

public class LoginScreen extends AppCompatActivity {

...
...

  public LoginScreen() {

    try {
        inStream = this.getApplicationContext().getResources().openRawResource(R.raw.mytruststore);

        KeyStore ks = KeyStore.getInstance("BKS");
        ks.load(inStream, "bks*password".toCharArray());
        inStream.close();

        KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
        kmf.init(ks, "bks*password".toCharArray());

        TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
        tmf.init(ks);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(kmf.getKeyManagers(),tmf.getTrustManagers(), null);
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        hurlStack = new HurlStack(null, sslSocketFactory);
    } catch (Exception e){
        Log.d("Exception:",e.toString());
    }

  }

...
...

}

Anyway, I'm still having problems..

Response: com.android.volley.NoConnectionError: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

Yama answered 18/9, 2016 at 4:8 Comment(2)
KeyManagerFactory is not needed if it is not required Client authentication. You can omit it. Trust anchor for certification path not found means the SSL client is not finding the server certificate into truststore. Are you sure the BKS contains the server certificate? have you imported the root CA or the final certificate? If you have imported the root CA, may be is a misconfiguration in server side. Try to import the leaf certificateBudget
this might help: https://mcmap.net/q/668201/-volley-trust-additional-root-certMusquash
M
8

i have implemented https by creating new requestQueue in my volley class by the following code

public RequestQueue getRequestQueue() {
    if (mRequestQueue == null) {
        mRequestQueue = Volley.newRequestQueue(getApplicationContext(), new HurlStack(null, newSslSocketFactory()));

    }

    return mRequestQueue;
}

private SSLSocketFactory newSslSocketFactory() {
    try {
        // Get an instance of the Bouncy Castle KeyStore format
        KeyStore trusted = KeyStore.getInstance("BKS");
        // Get the raw resource, which contains the keystore with
        // your trusted certificates (root and any intermediate certs)
        InputStream in = getApplicationContext().getResources().openRawResource(R.raw.keystore);
        try {
            // Initialize the keystore with the provided trusted certificates
            // Provide the password of the keystore
            trusted.load(in, KEYSTORE_PASSWORD);
        } finally {
            in.close();
        }

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

        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, tmf.getTrustManagers(), null);

        SSLSocketFactory sf = context.getSocketFactory();
        return sf;
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}
Mientao answered 1/3, 2017 at 9:28 Comment(6)
what is raw.keystore ?Tracee
folder raw , you place the keystore in itMientao
How do I make this keystore?Tracee
you extract it from your certificate file generated from the serverMientao
only sharing what I just practiced. as of how to generate certificate file from server, I use this tutorial wikihow.com/Export-Certificate-Public-Key-from-ChromeOsmen
and to create keystore using the certificate file, I used portecleOsmen
D
3

I have faced the similar problem in the past and the solution for the same that worked was to install the intermediate certificate authority on the server side.

What is interesting to note here is that visiting this server in most desktop browsers does not cause an error like a completely unknown CA or self-signed server certificate would cause. This is because most desktop browsers cache trusted intermediate CAs over time. Once a browser has visited and learned about an intermediate CA from one site, it won't need to have the intermediate CA included in the certificate chain the next time.

Some sites do this intentionally for secondary web servers used to serve resources. For example, they might have their main HTML page served by a server with a full certificate chain, but have servers for resources such as images, CSS, or JavaScript not include the CA, presumably to save bandwidth. Unfortunately, sometimes these servers might be providing a web service you are trying to call from your Android app, which is not as forgiving.

Configure the server to include the intermediate CA in the server chain. Most CAs provide documentation on how to do this for all common web servers. This is the only approach if you need the site to work with default Android browsers at least through Android 4.2.

You can follow the steps as mentioned here Missing intermediate certificate authority

Another example What is an intermediate certificate?

FYI trust-anchor-not-found-for-android-ssl-connection

Browsers may accept root certificate authority but Android SDK may not do the same because browsers cache the same. Browsers will cache intermediate certificates, and use them between different sites. Because of that, if you are missing the intermediate certificate, random users will receive a trust error, while others won't. Do intermediate certificates get cached in Firefox?

Doer answered 22/2, 2017 at 13:21 Comment(1)
you're a life saver, two days looking into my code and it was the server's fault.Dogwatch
T
0

you should accept all SSL certificates. To tell volley to trust your SSL, one option is the following code, which is based on https://developer.android.com/training/articles/security-ssl.html

// trust the SSL certificate in our smarter server
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    InputStream caInput = ctx.getAssets().open("smarter_ssl.crt");
    Certificate ca;
    try {
        ca = cf.generateCertificate(caInput);
    } finally {
        caInput.close();
    }

    // Create a KeyStore containing our trusted CAs
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);

    // Create a TrustManager that trusts the CAs in our KeyStore
    String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(keyStore);
    // Create an SSLContext that uses our TrustManager
    SSLContext context = SSLContext.getInstance("TLS");
    context.init(null, tmf.getTrustManagers(), null);

    SSLSocketFactory i = context.getSocketFactory();

    // Tell volley to use a SocketFactory from our SSLContext
    RequestQueue requestQueue = Volley.newRequestQueue(ctx.getApplicationContext(), new HurlStack(null, context.getSocketFactory()));

to convert .pfx ssl certificates to .crt, follow: https://www.ibm.com/support/knowledgecenter/SSVP8U_9.7.0/com.ibm.drlive.doc/topics/r_extratsslcert.html

The .crt file should be put in the assets folder as text.

Toil answered 27/10, 2020 at 22:47 Comment(2)
This works. As for caInput, you can also use ByteArrayInputStream(certificate.getBytes()). Where certificate is a String certificate. For the certificate, you need to supply x509 formatted server certificate (a.k.a server public). Try your self-signed openssl certificate first on the browser. If it works, then you can use the server certificate. Also, i use Intermediate CA signed certificate. Not a self-signed one. Perhaps the self-signed one could also work. I haven't tried it though.Tyrosine
"you should accept all SSL certificates". No, you really shouldn't. You should only trust a certificate signed by a CA you trust, or indeed your own root CA certificate if self-signing.Vanhook

© 2022 - 2024 — McMap. All rights reserved.