My android application connects to an URL provided by the user. In case of HTTPS connections, if the server's certificate is issued by a CA that already exists in the Android's TrustManager, everything is fine.
But if the server uses a self-signed certificate how can I obtain that certificate and store it in the TrustManager on first connection?
I am using OkHttp library for performing network tasks. The solution that I have currently forces me to add the certificate in the application's raw folder during development but this will not work for the above mentioned scenario. The code that I am using currently is as below:
private KeyStore readKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
String password = "testPass";
InputStream is = null;
try {
is = activity.getApplicationContext().getResources().openRawResource(R.raw.server_key);
ks.load(is, password.toCharArray());
} finally {
if (is != null)
is.close();
}
return ks;
}
private OkHttpClient getOkHttpClient() throws CertificateException, IOException, KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(readKeyStore());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(readKeyStore(), "testPass".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
return new OkHttpClient().setSslSocketFactory(sslContext.getSocketFactory());
}
The simple solution
Here's a code example of a trust manager callback. From the callback you can either store the self-signed certificates or just accept them right away. But you SHOULD NOT do neither. By accepting self-signed certs, you are subject to connecting to fake or malicious servers which can steal personal data, install malware, and do other nasty things.
The better-than-simple solution
If a server offers a self-signed certificate that you, the developer, trust at compile time, you can bundle the cert into the app like you're already doing. It would be much better if the server had a CA-signed certificate, but if you really need to connect to a server that offers a self-signed cert, this will (have to) do.
A nicer solution
Never accept self-signed certificates in runtime. If you want to allow your users to connect to these kind of servers, you can show a warning message like "proceed at your own risk" before accepting the self-signed cert.
You should not add it on first connection. Ideally they should just pay to get a proper certificate signed by a CA.
But even then you might find popular new CAs like Let's Encrypt that are not supported by the installed Java version.
In these cases you can either add the individual cert or the new CA manually using keytool for a JVM, or load it via standard Android mechanisms.
Or you can bundle those with the app. This example code shows how to load bundles certificates in addition to existing system certs using a Merged TrustStore https://github.com/yschimke/oksocial/blob/master/src/main/java/com/baulsupp/oksocial/security/CertificateUtils.java
Related
I have a problem with volley and SSL and some old Android devices.
Problem is that the root certificate we use that is publicly trusted is not in place in these old devices and the OS is no longer updated so I thought I would add the CA as a file. I found lots of examples on how to trust a single certificate but I also want to keep trusting the existing root certificates. I just want to add a few to the trust store used by our app.
Is this possible? Is there a code example of this somewhere?
EDIT:
I have checked the following links, all deal with self-signed or a fully custom CA store or simply disable the checks which I do not want to do. I want to keep the default CA store but add one or two additional CAs
How to import self-signed SSL certificate to Volley on Android 4.1+
How can I make Android Volley perform HTTPS request, using a certificate self-signed by an Unknown CA?
Kind regards
Jens
What you can do is include the certificate within your app and load it programatically and supply it to your volley client.
The links you have shared gives me the idea that it should be possible, however I have never used volley and cannot confirm if it will actually work. So I will do an attempt and hopefully you can test it our and share your results here.
KeyStore baseTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
baseTrustStore.load(null, null);
// get your custom certificate(s) from your android device our within your app
// and load it as a certificate object
Certificate myTrustedCertificate = // your additional trusted certificate
baseTrustStore.setCertificateEntry("my-trusted-certificate", myTrustedCertificate);
int counter = 0;
KeyStore systemTrustStore = KeyStore.getInstance("AndroidCAStore");
Enumeration<String> aliases = systemTrustStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (systemTrustStore.isCertificateEntry(alias)) {
Certificate certificate = systemTrustStore.getCertificate(alias);
baseTrustStore.setCertificateEntry("" + counter++, certificate);
}
}
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(baseTrustStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
HurlStack hurlStack = new HurlStack(null, socketFactory);
RequestQueue queue = Volley.newRequestQueue(this, hurlStack);
So I first create an empty trust store where I will add all the certificates which I want to trust. First add the custom root certificate which you have somewhere and map it to an instance of java.security.cert.Certificate. Afterwords get the android CA store and extract all the trusted certificates. Afterwords add all these certificates to your base trust store which you can use to create a TrustManagerFactory, SSLContext and SSLSocketFactory. The counter within the example is just a way to generate some alias, but you can specify or generate your own.
I could ask user to install my PKCS12 CA certificate using createInstallIntent of Android keychain. Now I am trying to fetch back that installed certificate from keychain through KeyChain.choosePrivateKeyAlias. Here when a user selects a cert, I could get a call back to alias. With that alias I could get the private key and cert chain programatically. Now I want to know; by user selecting this cert does it means that selected cert will be added (by OS) to all subsequent network calls made by the app? or do I still need to add that cert manually to my network requests?
I use a Volley for network operations and until now I have been adding a PKCS12 cert through the following code:
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(
new ByteArrayInputStream(Base64Helper.decodeIntoByteArray(PKCS12_CERT_AS_STRING),
PKCS12_PASSWORD.toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, PKCS12_PASSWORD.toCharArray());
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
HurlStack hurlStack = new HurlStack(null, sslContext.getSocketFactory());
requestQueue = Volley.newRequestQueue(MyApplication.getInstance().getAppContext(), hurlStack);
So after installing My CA cert to android keychain and making user to choose that cert in the system prompt; do I still need to do the above process to add the cert to my volley request or will this be taken care by Android OS?
I'm currently writing my first android app and decided to use google cloud endpoints.
I have secure my backend endpoints methods by following this docs : https://cloud.google.com/appengine/docs/java/endpoints/auth,
https://cloud.google.com/appengine/docs/java/endpoints/gen_clients, https://cloud.google.com/appengine/docs/java/endpoints/consume_android
Basically you have to generate WEB_CLIENT_ID and ANDROID_CLIENT_ID for your endpoints,
add a param User on each method you want to secure and finally check inside the method that
the user is not null otherwise throw OAuthRequestException.
I have build the client android library and configure it like this in my android app :
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String accountName = preferences.getString(MainActivity.PREF_ACCOUNT_NAME, null);
GoogleAccountCredential credential = GoogleAccountCredential.usingAudience(context,
"server:client_id:xxxxxxxxx-xxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com");
credential.setSelectedAccountName(accountName);
DataLoaderApi.Builder builder = new DataLoaderApi.Builder(AndroidHttp.newCompatibleTransport(),
new AndroidJsonFactory(), credential);
At first it seems to work pretty well you cannot use the backend methods without credential. (You will have 401 error)
Unfortunately I notice that it's still possible to decrypt https request/response to backend endpoints if you use a tool like "Charles Proxy"
(https://www.charlesproxy.com/). You just have to install charles proxy certificate in your smartphone and configure the proxy.
Here's another description of the same problem:
http://nickfishman.com/post/50557873036/reverse-engineering-native-apps-by-intercepting-network
Now you have the endpoints url used by the android app, you've got all the param sent in each request and all the header properties even the autorization property
with the token so basically all the informations you need to use my backend endpoints in your app. :(
I know the token will expire after a while but in the meantime you can extract my datas.
Google speak about that in the google cloud store doc :
https://cloud.google.com/storage/docs/concepts-techniques#bestpractices
"Make sure that you use an HTTPS library that validates server certificates. A lack of server certificate validation makes your application vulnerable to man-in-the-middle attacks or other attacks."
Great but when you use the endpoints client library generated with their tool you are vulnerable too.
I know there are solution to avoid this problem:
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning
But how can I use the generated endpoints client android library and avoid man-in-the-middle attack ?
Create your custom KeyStore and put only system certs in it. Android system certs can be obtained from here.
Create TrustManager and SSLSocketFactory which will trust only this KeyStore which you've just created, referenced from this:
// Load CAs from an InputStream
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));
Certificate ca;
try {
ca = cf.generateCertificate(caInput);
System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} 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 ssf = context.getSocketFactory();
Create NetHttpTransport using NetHttpTransport.Builder, and set created SSLSocketFactory above. Then you can use your endpoint method builder as usual:
Api.Builder builder = new Api.Builder(objNetHttpTransport,
new AndroidJsonFactory(), credential);
This should work, only prob is in managing root certs. You will need to send updates regularly.
I would like to know if there is any way to make my hosting only accept connections from my App, and reject the connections from any web browser.
The best approach is probably going to be to use SSL with client certificates. This will give you authentication as well as encryption.
You're probably familiar with using SSL via HTTPS to secure your web browser when you pay for something online. The process is similar except that the server has to also be configured to require a client-side certificate. In Apache, this is a setting called SSLVerifyClient. This will force the web server to require a certificate from the client and then to verify it as valid.
Assigning client-side certificates from a root authority like Verisign can be expensive, so you probably want to generate your own. You can do this using OpenSSL.
Once you have a client certificate, you have to copy the certificate into your application storage on the device. Then you can use the Keystore object to open it. Once it's open, you can associate it with your HTTP requests when you use the HttpUrlConnection object.
Since you'll probably generate your own certificate instead of using those from a root authority, you will need to setup a TrustManager in your Android app to accept your self-signed certificates and associate that with the HttpUrlConnection when you make the request.
That's the general strategy and that should be enough to get you started. If you get stuck on something specific, try asking individual questions about the separate parts. The Apache and OpenSSL part would probably be better posted on Server Fault.
Here are some links for the server-side stuff:
Client certificates with apache
Client-authenticated_TLS_handshake
Here are links for the Android development:
HTTPS with Client Certificates on Android
android-ssl
FakeX509TrustManager (useful during development)
It looks like all you need are in those links, but I'll summarize the Android code here. Note that I don't have a test environment for this, so the code is incomplete and untested. Again, this is just to get you started.
// open the certificate
keyStore = KeyStore.getInstance("PKCS12");
fis = new FileInputStream(certificateFile);
keyStore.load(fis, clientCertPassword.toCharArray());
// create the SSL context
KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
kmf.init(keyStore, clientCertPassword.toCharArray());
KeyManager[] keyManagers = kmf.getKeyManagers();
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
// perform the HTTP request
String result = null;
HttpURLConnection urlConnection = null;
try {
URL requestedUrl = new URL(url);
urlConnection = (HttpURLConnection) requestedUrl.openConnection();
if(urlConnection instanceof HttpsURLConnection) {
((HttpsURLConnection)urlConnection)
.setSSLSocketFactory(sslContext.getSocketFactory());
}
urlConnection.setRequestMethod("GET");
urlConnection.setConnectTimeout(1500);
urlConnection.setReadTimeout(1500);
lastResponseCode = urlConnection.getResponseCode();
result = IOUtil.readFully(urlConnection.getInputStream());
lastContentType = urlConnection.getContentType();
} catch(Exception ex) {
result = ex.toString();
} finally {
if(urlConnection != null) {
urlConnection.disconnect();
}
}
I am trying to get two way SSL authentication working between a Python server and an Android client application. I have access to both the server and client, and would like to implement client authentication using my own certificate. So far I have been able to verify the server certificate and connect without client authentication.
What sort of certificate does the client need and how do I get it to automatically send it to the server during the handshake process? Here is the client and server side code that I have so far. Is my approach wrong?
Server Code
while True: # Keep listening for clients
c, fromaddr = sock.accept()
ssl_sock = ssl.wrap_socket(c,
keyfile = "serverPrivateKey.pem",
certfile = "servercert.pem",
server_side = True,
# Require the client to provide a certificate
cert_reqs = ssl.CERT_REQUIRED,
ssl_version = ssl.PROTOCOL_TLSv1,
ca_certs = "clientcert.pem", #TODO must point to a file of CA certificates??
do_handshake_on_connect = True,
ciphers="!NULL:!EXPORT:AES256-SHA")
print ssl_sock.cipher()
thrd = sock_thread(ssl_sock)
thrd.daemon = True
thrd.start()
I suspect I may be using the wrong file for ca_certs...?
Client Code
private boolean connect() {
try {
KeyStore keystore = KeyStore.getInstance("BKS"); // Stores the client certificate, to be sent to server
KeyStore truststore = KeyStore.getInstance("BKS"); // Stores the server certificate we want to trust
// TODO: change hard coded password... THIS IS REAL BAD MKAY
truststore.load(mSocketService.getResources().openRawResource(R.raw.truststore), "test".toCharArray());
keystore.load(mSocketService.getResources().openRawResource(R.raw.keystore), "test".toCharArray());
// Use the key manager for client authentication. Keys in the key manager will be sent to the host
KeyManagerFactory keyFManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyFManager.init(keystore, "test".toCharArray());
// Use the trust manager to determine if the host I am connecting to is a trusted host
TrustManagerFactory trustMFactory = TrustManagerFactory.getInstance(TrustManagerFactory
.getDefaultAlgorithm());
trustMFactory.init(truststore);
// Create the socket factory and add both the trust manager and key manager
SSLCertificateSocketFactory socketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory
.getDefault(5000, new SSLSessionCache(mSocketService));
socketFactory.setTrustManagers(trustMFactory.getTrustManagers());
socketFactory.setKeyManagers(keyFManager.getKeyManagers());
// Open SSL socket directly to host, host name verification is NOT performed here due to
// SSLCertificateFactory implementation
mSSLSocket = (SSLSocket) socketFactory.createSocket(mHostname, mPort);
mSSLSocket.setSoTimeout(TIMEOUT);
// Most SSLSocketFactory implementations do not verify the server's identity, allowing man-in-the-middle
// attacks. This implementation (SSLCertificateSocketFactory) does check the server's certificate hostname,
// but only for createSocket variants that specify a hostname. When using methods that use InetAddress or
// which return an unconnected socket, you MUST verify the server's identity yourself to ensure a secure
// connection.
verifyHostname();
// Safe to proceed with socket now
...
I have generated a client private key, a client certificate, a server private key, and a server certificate using openssl. I then added the client certificate to keystore.bks (which I store in /res/raw/keystore.bks) I then added the server certificate to the truststore.bks
So now when the client tries to connect I am getting this error server side:
ssl.SSLError: [Errno 1] _ssl.c:504: error:140890C7:SSL routines:SSL3_GET_CLIENT_CERTIFICATE:peer did not return a certificate
And when I try to do this in the android client
SSLSession s = mSSLSocket.getSession();
s.getPeerCertificates();
I get this error:
javax.net.ssl.SSLPeerUnverifiedException: No peer certificate
So obviously the keystore I am using doesn't appear to have a correct peer certificate in it and thus isn't sending one to the server.
What should I put in the keystore to prevent this exception?
Furthermore, is this method of two way SSL authentication safe and effective?
The server needs to trust the client certificate. The usual way to do this is to create a CA, then have it sign a server certificate and a client certificate. Each one would have the CA certificate in their respective trust stores. Then you need to initialize the SSLContext with something like this:
KeyStore trustStore = loadTrustStore();
KeyStore keyStore = loadKeyStore();
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
KeyManagerFactory kmf = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, KEYSTORE_PASSWORD.toCharArray());
SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
You can then use the SSLContext to create socket factories as needed. They will be initialized with the proper keys and certificates.
update
I'm having trouble getting a peer certificate added to the keystore,
and having that certificate sent to the server.
Done a little research that may aid your journey. mvsjes2 reported that incorrect ports can cause SSLPeerUnverifiedException to be thrown. Verify you are using port 443 as I can't see in your code where you set's the port.
Also check out emmby answer which I found also insightful.
https://stackoverflow.com/a/6378872/821312
https://stackoverflow.com/a/12012622/821312
Original post
So obviously the keystore I am using doesn't appear to have a correct
peer certificate in it and thus isn't sending one to the server.
Try looking at this article about using the connect to Unknown Certificates authority, which will let you use certificate that is not default with Android.. You might have to include a public/intermediate certificate in your application to ensure that existing/older devices will be able to connect with your server. Follow the article and see if that solved the problem of yours.
Furthermore, is this method of two way SSL authentication safe and effective?
Only if you can get it work first! As long as your code can checks the validity of the SSL certificate it should be effective enough. Safety wise... well as long the ssl certificate itself is created with a strong RSA signature/key it will make it more secure than just plain ol' http. Let me know if you continue to have issues.