I'm trying to send a request to a web api in Xamarin.Android. The api requires a client certificate. I followed the advice in this question: xamarin.ios httpclient clientcertificate not working with https, but I get a "method not implemented" exception. Can anyone help?
Here's my code:
string result = await CallApi(new System.Uri("myurl"));
protected async Task<string> CallApi(Uri url)
{
try
{
AndroidClientHandler clientHandler = new AndroidClientHandler();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Ssl3;
using (var mmstream = new MemoryStream())
{
Application.Context.Assets.Open("mycert.pfx").CopyTo(mmstream);
byte[] b = mmstream.ToArray();
X509Certificate2 cert = new X509Certificate2(b, "password", X509KeyStorageFlags.DefaultKeySet);
clientHandler.ClientCertificates.Add(cert);
}
ServicePointManager.ServerCertificateValidationCallback += new RemoteCertificateValidationCallback((sender, certificate, chain, policyErrors) => { return true; });
HttpClient client = new HttpClient(clientHandler);
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseBody);
return responseBody;
}
catch (HttpRequestException e)
{
Console.WriteLine("\nException Caught!");
return string.Empty;
}
}
In the post you mentioned probably the managed handler is used. Since this handler currently doesn't support TLS 1.2 you shouldn't use it, but instead really use the AndroidClientHandler (see also Xamarin and TLS 1.2).
Unfortunately ClientCertificates is indeed not implemented in AndroidClientHandler.
If you want to use client certificate with android you can extend the AndroidClientHandler:
using Java.Security;
using Java.Security.Cert;
using Javax.Net.Ssl;
using Xamarin.Android.Net;
using Xamarin.Forms;
public class AndroidHttpsClientHandler : AndroidClientHandler
{
private SSLContext sslContext;
public AndroidHttpsClientHandler(byte[] customCA, byte[] keystoreRaw) : base()
{
IKeyManager[] keyManagers = null;
ITrustManager[] trustManagers = null;
// client certificate
if (keystoreRaw != null)
{
using (MemoryStream memoryStream = new MemoryStream(keystoreRaw))
{
KeyStore keyStore = KeyStore.GetInstance("pkcs12");
keyStore.Load(memoryStream, clientCertPassword.ToCharArray());
KeyManagerFactory kmf = KeyManagerFactory.GetInstance("x509");
kmf.Init(keyStore, clientCertPassword.ToCharArray());
keyManagers = kmf.GetKeyManagers();
}
}
// custom truststore if you have your own ca
if (customCA != null)
{
CertificateFactory certFactory = CertificateFactory.GetInstance("X.509");
using (MemoryStream memoryStream = new MemoryStream(customCA))
{
KeyStore keyStore = KeyStore.GetInstance("pkcs12");
keyStore.Load(null, null);
keyStore.SetCertificateEntry("MyCA", certFactory.GenerateCertificate(memoryStream));
TrustManagerFactory tmf = TrustManagerFactory.GetInstance("x509");
tmf.Init(keyStore);
trustManagers = tmf.GetTrustManagers();
}
}
sslContext = SSLContext.GetInstance("TLS");
sslContext.Init(keyManagers, trustManagers, null);
}
protected override SSLSocketFactory ConfigureCustomSSLSocketFactory(HttpsURLConnection connection)
{
SSLSocketFactory socketFactory = sslContext.SocketFactory;
if (connection != null)
{
connection.SSLSocketFactory = socketFactory;
}
return socketFactory;
}
}
If you refer to AndroidClientHandler Source Code, you can find following statement:
AndroidClientHandler also supports requests to servers with "invalid" (e.g. self-signed) SSL certificates. Since this process is a bit convoluted using
the Java APIs, AndroidClientHandler defines two ways to handle the situation. First, easier, is to store the necessary certificates (either CA or server certificates)
in the collection or, after deriving a custom class from AndroidClientHandler, by overriding one or more methods provided for this purpose(, and ). The former method should be sufficient for most use cases...
So, for usage of AndroidClientHandler you should use clientHandler.TrustedCerts together with Java.Security.Cert.X509Certificate:
Java.Security.Cert.X509Certificate cert = null;
try
{
CertificateFactory factory = CertificateFactory.GetInstance("X.509");
using (var stream = Application.Context.Assets.Open("MyCert.pfx"))
{
cert = (Java.Security.Cert.X509Certificate)factory.GenerateCertificate(stream);
}
} catch (Exception e)
{
System.Console.WriteLine(e.Message);
}
if (clientHandler.TrustedCerts != null)
{
clientHandler.TrustedCerts.Add(cert);
}
else
{
clientHandler.TrustedCerts = new List<Certificate>();
clientHandler.TrustedCerts.Add(cert);
}
Notes: don't use Application.Context.Assets.Open("ca.pfx").CopyTo(mmstream); otherwise you will get inputstream is empty exception.
Related
From a result of security scan program, I have a need to restrict the Certificate Authorities the app trusts.
The scan result points out the line at webView.loadUrl("https://example.com/page");. I see how I can create a SslSocketFactory that uses my TrustManager, but I don't see an API in WebView that allows me to set that.
https://developer.android.com/training/articles/security-ssl.html#UnknownCa
What are some possible ways to achieve this?
I think WebViewClient 's onReceivedSslError method will be a good entry point.
First of all, follow the exact same snippet from https://developer.android.com/training/articles/security-ssl.html#UnknownCa to prepare TrustManager.
TrustManagerFactory tmf = null;
private void initTrustStore() throws
java.security.cert.CertificateException, FileNotFoundException,
IOException, KeyStoreException, NoSuchAlgorithmException {
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore trustedKeyStore = KeyStore.getInstance(keyStoreType);
trustedKeyStore.load(null, null);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = new BufferedInputStream(
getResources().getAssets().open("ca.crt"));
Certificate ca;
try {
ca = cf.generateCertificate(caInput);
Log.d(TAG, "ca-root DN=" + ((X509Certificate) ca).getSubjectDN());
}
finally {
caInput.close();
}
trustedKeyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(trustedKeyStore);
}
Then, extends custom WebViewClient class, checking snippet from https://stackoverflow.com/a/6379434/1099884
private class CheckServerTrustedWebViewClient extends WebViewClient{
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
Log.d(TAG, "onReceivedSslError");
boolean passVerify = false;
if(error.getPrimaryError() == SslError.SSL_UNTRUSTED){
SslCertificate cert = error.getCertificate();
String subjectDN = cert.getIssuedTo().getDName();
Log.d(TAG, "subjectDN: "+subjectDN);
try{
Field f = cert.getClass().getDeclaredField("mX509Certificate");
f.setAccessible(true);
X509Certificate x509 = (X509Certificate)f.get(cert);
X509Certificate[] chain = {x509};
for (TrustManager trustManager: tmf.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
X509TrustManager x509TrustManager = (X509TrustManager)trustManager;
try{
x509TrustManager.checkServerTrusted(chain, "generic");
passVerify = true;break;
}catch(Exception e){
Log.e(TAG, "verify trustManager failed", e);
passVerify = false;
}
}
}
Log.d(TAG, "passVerify: "+passVerify);
}catch(Exception e){
Log.e(TAG, "verify cert fail", e);
}
}
if(passVerify == true)handler.proceed();
else handler.cancel();
}
}
Finally, set the CheckServerTrustedWebViewClient to WebView
webView.setWebViewClient(new CheckServerTrustedWebViewClient());
However, there is one problem. The prepared CA certificate is the exact one sign the server one (intermediate-CA NOT root CA). Only provide root CA certificate will not work. Isn't TrustManager can download server certificate chain on runtime? Any suggestion?
The doc seems to be updated:
Fortunately, you can teach your application to trust custom CAs by configuring your application's Network Security Config, without needing to modify the code inside your application.
blog post I think to be related
I am using Retrofit and OkHttp to perform all network operations like GET, POST for both HTTP and HTTPS url. Everything is working fine but except that i have a requirement to reuse the sessions in order to reduce the Handshake timing process for each and every service calls. As of now the server takes more than 800ms to initiate the handshake between client and server for all the service calls.
What I need:
I have to reuse the SSLSessions in order to make handshake happen only for the first time or during specific intervals.
Code I am using for SSL using Okhttp and Retrofit:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseURL)
.addConverterFactory(GsonConverterFactory.create())
.client(getOkHttpClient(context, new OkHttpClient(), context.getResources().openRawResource(R.raw.mysslcertificate)))
.build();
retrofit.create(apiClass);
public static OkHttpClient getOkHttpClient(Context context,OkHttpClient client, InputStream inputStream) {
try {
if (inputStream != null) {
SSLContext sslContext = sslContextForTrustedCertificates(inputStream);
if (sslContext != null) {
client = client.newBuilder()
.sslSocketFactory(sslContext.getSocketFactory()).build();
else {
CLog.i(Constants.LOG_TAG_HTTPLIBRARY,"GZip not done because it is not a Analytics data");
client = client.newBuilder()
.sslSocketFactory(sslContext.getSocketFactory()).build();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return client;
}
private static SSLContext sslContextForTrustedCertificates(InputStream in) {
try {
CertificateFactory e = CertificateFactory.getInstance("X.509");
Collection certificates = e.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
} else {
char[] password = "password".toCharArray();
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
Iterator keyManagerFactory = certificates.iterator();
while (keyManagerFactory.hasNext()) {
Certificate trustManagerFactory = (Certificate) keyManagerFactory.next();
String sslContext = Integer.toString(index++);
keyStore.setCertificateEntry(sslContext, trustManagerFactory);
}
KeyManagerFactory var10 = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
var10.init(keyStore, password);
TrustManagerFactory var11 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
var11.init(keyStore);
SSLContext var12 = SSLContext.getInstance("TLS");
var12.init(var10.getKeyManagers(), var11.getTrustManagers(), new SecureRandom());
return var12;
}
} catch (Exception var9) {
var9.printStackTrace();
}
return null;
}
What I have tried:
Since i couldn't find anything related to OkHttpClient but i tried referring few of the solutions like from the link as follows:
https://gist.github.com/codebutler/5565971
https://developer.android.com/reference/javax/net/ssl/SSLContext.html
But to be very frank nothing was helpful to me and I couldn't even find any relavant solutions for my requirement. In turn finally, I am completely stuck with this solution for the couple of weeks. Kindly help me to achieve my tasks through any of your tips and suggestions. Any piece of code or approach will be very useful to me. Thanks in advance.
I successfully got the server to use a certificate in the form of a JKS file. HTTPS is working as expected when used with web browsers and other web clients.
For Android, my team uses the following to persuade OkHttp to accept the certificate.
static KeyStore readKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException
{
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
// get user password and file input stream
char[] password = "password".toCharArray();
java.io.InputStream fis = null;
try {
fis = ServiceProducer.class.getClassLoader().getResourceAsStream("res/raw/keystore.jks");
ks.load(fis, password);
} catch (IOException e)
{
} finally
{
if (fis != null)
{
try
{
fis.close();
} catch (IOException e)
{
}
}
}
return ks;
}
The code that uses the key:
OkHttpClient.Builder builder = new OkHttpClient.Builder();
KeyStore keyStore = readKeyStore();
SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "password".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
builder.sslSocketFactory(sslContext.getSocketFactory());
OkHttpClient client = builder.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://192.168.5.91:9443")
.addConverterFactory(JacksonConverterFactory.create())
.client(client)
.build();
However, accessing the service throws the following exception:
java.security.cert.CertPathValidationException: Trust anchor for certification path not found.
Have we done the certificate installation correctly? Or are we facing a different kind of problem?
I want to use SSL Pinning in volley network library. Is there any way to implement SSL pinning with volley? Does volley provide this support for security improvements?
I just implemented it like described here: http://blog.ostorlab.co/2016/05/ssl-pinning-in-android-networking.html
Here is the needed code for a volley-implementation:
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// Generate the certificate using the certificate file under res/raw/cert.cer
InputStream caInput = new BufferedInputStream(getResources().openRawResource(R.raw.cert));
Certificate ca = cf.generateCertificate(caInput);
caInput.close();
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore trusted = KeyStore.getInstance(keyStoreType);
trusted.load(null, null);
trusted.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(trusted);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
SSLSocketFactory sf = context.getSocketFactory();
mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext(), new HurlStack(null, sf));
Seems to work!
I just looked into the same thing for a project I am working on. The position I am in may be different to you however.
I am using Volley with an OKHttp Network stack (https://gist.github.com/JakeWharton/5616899):
Add these to your Gradle Build:1
compile "com.squareup.okhttp:okhttp:2.7.5"
compile "com.squareup.okhttp:okhttp-urlconnection:2.7.5"
Add a OKHttpStack class;
public class OKHttpStack extends HurlStack {
private final OkUrlFactory okUrlFactory;
public OKHttpStack() {
this(new OkUrlFactory(
new OkHttpClient.Builder()
.certificatePinner(
new CertificatePinner.Builder()
.add("example.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") //This is the cert
.build())
.build();
));
}
public OKHttpStack(OkUrlFactory okUrlFactory) {
if (okUrlFactory == null) {
throw new NullPointerException("Client must not be null.");
}
this.okUrlFactory = okUrlFactory;
}
#Override
protected HttpURLConnection createConnection(URL url) throws IOException {
return okUrlFactory.open(url);
}
}
When you then create your RequestQueue do something like:
Network network = new BasicNetwork(new OKHttpStack());
File cacheDir = new File(context.getCacheDir(), "volley");
int threads = 4;
mRequestQueue = new RequestQueue(new DiskBasedCache(cacheDir), network, threads);
Please note I have yet to test this, we are thinking about pinning at the moment.
Good luck!
Gav
References:
https://gist.github.com/JakeWharton/5616899
https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/CertificatePinning.java
You can use public key pinning instead of certificate pinning:
Public Key Pinning with Volley Library
I am implementing the same exact thing. I found a blog post that will hopefully be of help to you
http://ogrelab.ikratko.com/using-android-volley-with-self-signed-certificate/
You can use network_security_config.xml, more info : https://developer.android.com/training/articles/security-config
I am trying to make a https request using this code:
RequestQueue queue = Volley.newRequestQueue(getApplicationContext());
request = new Request<String>(Request.Method.GET,"https://devblahblahblah.com/service/etc",errListener);
but I am getting this error:
com.android.volley.NoConnectionError:
javax.net.ssl.SSLHandshakeException:
java.security.cert.CertPathValidatorException: Trust anchor for
certification path not found.
Two points to be noted:
The HTTPS cert is valid. Easily opens without any warning on browser.
The above code works fine with HTTP links.
I actually need to know if there are any switches/options in the Android Volley framework by using which I'll successfully hit a HTTPS URL?
Warning: The following code should not be used in production because it is vulnerable to SSL attacks
Probably these codes below will be helpful for you:
1.Create a HttpsTrustManager class that implements X509TrustManager:
public class HttpsTrustManager implements X509TrustManager {
private static TrustManager[] trustManagers;
private static final X509Certificate[] _AcceptedIssuers = new X509Certificate[]{};
#Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] x509Certificates, String s)
throws java.security.cert.CertificateException {
}
#Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] x509Certificates, String s)
throws java.security.cert.CertificateException {
}
public boolean isClientTrusted(X509Certificate[] chain) {
return true;
}
public boolean isServerTrusted(X509Certificate[] chain) {
return true;
}
#Override
public X509Certificate[] getAcceptedIssuers() {
return _AcceptedIssuers;
}
public static void allowAllSSL() {
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
#Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
});
SSLContext context = null;
if (trustManagers == null) {
trustManagers = new TrustManager[]{new HttpsTrustManager()};
}
try {
context = SSLContext.getInstance("TLS");
context.init(null, trustManagers, new SecureRandom());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
HttpsURLConnection.setDefaultSSLSocketFactory(context
.getSocketFactory());
}
}
2.Add HttpsTrustManager.allowAllSSL() before you make a https request:
HttpsTrustManager.allowAllSSL();
String tag_string_req = "string_req";
StringRequest strReq = new StringRequest(Request.Method.POST,
your_https_url, new Response.Listener<String>() {
#Override
public void onResponse(String response) {
Log.d(TAG, "response :"+response);
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
VolleyLog.d(TAG, "Error: " + error.getMessage());
}
}){
#Override
protected Map<String, String> getParams() {
Map<String, String> params = new HashMap<String, String>();
params.put("username", "max");
params.put("password", "123456");
return params;
}
};
AppController.getInstance().addToRequestQueue(strReq, tag_string_req);
you can add this class and execut it from onCreate method
new NukeSSLCerts().nuke();
it will make volley to Trust all SSL certificates.
So far the only answer talk about adding an untrusted certificate as the solution, but since your browser doesn't complain it usually means Volley can't find the intermediate certificate that does complete the full trusted chain.
It happened to me with LetsEncrypt certificates. Most browsers already have that intermediate certs so on browser everything looks fine, but Volley was apparently missing something.
The solution
Add the intermediate certificate to your webserver config. For Apache you can follow this reference:
https://access.redhat.com/solutions/43575
For LetsEncrypt it specifically is this file: /etc/letsencrypt/live/your.website.com/chain.pem
So besides your CertificateFile and KeyFile you should already have working you now have this third line:
SSLCertificateChainFile /etc/letsencrypt/live/your.website.com/chain.pem
Just adding that line, restarting apache and Volley doesn't complain anymore and you didn't introduce any security vulnerabilities!
If you are using volley and want to HTTPS request or SSL Certified service then you can choose this easiest way : -->
Step --> 1. keep .cer file into res/raw/ folder.
Step --> 2. Use this method and replace .cer file name with your .cer file and replace your host name also.
private SSLSocketFactory getSocketFactory() {
CertificateFactory cf = null;
try {
cf = CertificateFactory.getInstance("X.509");
InputStream caInput = getResources().openRawResource(R.raw.cert_name);
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("10.199.89.68")==0; //The Hostname of your server.
}
};
HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
SSLContext context = null;
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;
}
Step --> 3. Replace this line "RequestQueue queue = Volley.newRequestQueue(this);" with "RequestQueue queue = Volley.newRequestQueue(this, new HurlStack(null, getSocketFactory()));" in request of volley.
I couldn't open the link provided by #Ogre_BGR,but while browsing the net I found the actual implementation done in following smanikandan14 Github.Look upon his SSl-connection explanation to understand more about it.
This can happen for several reasons, including:
The CA that issued the server certificate was unknown
The server certificate wasn't signed by a CA, but was self signed
The server configuration is missing an intermediate CA
Official doc from android
Solution:
you can provide a certificate file within the request
For anyone who will come up against a problem like this and you use Letsencrypt for your SSL and node.js for webserver, try this. Assuming you have something like this. I fixed this by adding the line const chain = fs... Hope this helps
...
const app = express();
const privateKey = fs.readFileSync('ssl/privkey.pem', 'utf8');
const certificate = fs.readFileSync('ssl/cert.pem', 'utf8');
const chain = fs.readFileSync('ssl/chain.pem', 'utf8');
const credentials = {key: privateKey, cert: certificate, ca: chain};
...
var httpsServer = https.createServer(credentials, app);
I got the same problem when I add ssl to the domain, After 2 days gone, I found the solution the URL is getting wrong . I was using https://example.com but when I add ssl into domain the url will be change
https://www.example.com
And POST is working fine
got this error when i turned off proxy from cloudflare
check image here
the best solution for this problem is you can turn on proxy back and also add a full secure access on ssl certificate.
If anyone is using nginx and SSL certificates from letsencrypt, the solution is to simply use the certificate from file fullchain.pem instead of cert.pem:
ssl_certificate /.../fullchain.pem;
This file includes the concatenation of your certificate and the CA's.