Greetings,
I am trying to create an Android test application that will only perform the following action 'Connect to a JSON WCF rest service in IIS with self-signed client-certificate authentication', however at the moment I keep receiving 'forbidden access' when I run the application. A client WCF to the WCF service seems to work fine and the Android client application to the WCF service also works when I disabled 'require client certificates'.
The strange part is that Eclipse informs that the client-certificate is found and that a KeyManager is created with it, but nothing is send to the server.
The following steps have been undertaken for the self-signed certificates
For the handling of the self-signed certificates in Android I tried to use several examples however the EasySSLSocketFactory and EasySSLTrustManager from StackOverflow:self-signed-ssl-acceptance-android worked for the most part. I also tried creating the keystores with the default keytool, however this result into more incorrect keystores being created.
Update 2011-03-17: system information
The system operating system that is hosting IIS is Windows XP with IIS-5 with .NET 4.0.
The service in IIS has the serverCA.cer assigned as server certificate and the require client certificates is enabled.
The android version I'm working on is 2.3.3 with Eclipse and have set the permission for Internet and have the keystore and truststore added as a raw resource in the Eclipse project.
Additionally when I look up in debug mode what the KeyManagerFactory.getKeyManagers() return I see that there is one item in the list.
Here are details of actions/code that I use with the issue:
The certificates were created with makecert as it first had to work between a WCF service and client.
makecert.exe -r -n "CN=rootCA,O=Organization,OU=Org Unit,L=Location,S=SH,C=Country" -pe -ss root -sr LocalMachine -sky exchange -m 96 -a sha1 -len 2048 rootCA.cer -sv rootCA.pvk
makecert.exe -n "CN=serverCA" -pe -ss my -sr LocalMachine -sky exchange -m 96 -in "rootCA" -is root -ir LocalMachine -a sha1 -eku 1.3.6.1.5.5.7.3.1,1.3.6.1.5.5.7.3.2 serverCA.cer
makecert.exe -n "CN=clientCA" -pe -ss my -sr CurrentUser -sky exchange -m 96 -in "rootCA" -is root -ir LocalMachine -a sha1 -eku 1.3.6.1.5.5.7.3.2 clientCA.cer -sv clientCA.
pvk2pfx.exe -pvk clientCA.pvk -spc clientCA.cer -pfx clientCA.pfx
The WCF configured is as followed:
<?xml version="1.0"?>
<configuration>
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="consoleOutputBehavior" type="JsonTestService.ConsoleOutputBehaviorExtensionElement, JsonTestService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<standardEndpoints>
<webHttpEndpoint>
<standardEndpoint name="JsonStandardEndpoint" defaultOutgoingResponseFormat="Json"
automaticFormatSelectionEnabled="true">
<security mode="Transport">
<transport clientCredentialType="Certificate" proxyCredentialType="None" realm="" />
</security>
</standardEndpoint>
</webHttpEndpoint>
</standardEndpoints>
<bindings>
<webHttpBinding>
<binding name="JsonBinding">
<security mode="Transport">
<transport clientCredentialType="Certificate" proxyCredentialType="None" realm="" />
</security>
</binding>
</webHttpBinding>
</bindings>
<serviceHostingEnvironment aspNetCompatibilityEnabled="false" />
<behaviors>
<endpointBehaviors>
<behavior name="jsonBehavior">
<webHttp defaultBodyStyle="Wrapped" defaultOutgoingResponseFormat="Json" />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="defaultBehavior">
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceCredentials>
<clientCertificate>
<authentication certificateValidationMode="Custom" mapClientCertificateToWindowsAccount="false"
customCertificateValidatorType="JsonTestService.CustomX509CertificateValidator, JsonTestService"
/>
</clientCertificate>
<serviceCertificate findValue="serverCA" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="defaultBehavior" name="JsonTestService.TestService">
<endpoint address="json" behaviorConfiguration="jsonBehavior"
binding="webHttpBinding" bindingConfiguration="JsonBinding"
name="JsonEndpoint" contract="JsonTestService.ITestService" kind="webHttpEndpoint"
endpointConfiguration="JsonStandardEndpoint">
</endpoint>
</service>
</services>
</system.serviceModel>
<system.web>
<authentication mode="None" />
</system.web>
</configuration>
The Object of the WCF service
namespace JsonTestService{
///
/// DataContract
///
[DataContract(Name = "Foo", Namespace = "http://www.example.com/data")]
public class FooDataContract
{
[DataMember(Order = 0)]
public string Item { get; set; }
[DataMember(Order = 1)]
public int Count { get; set; }
}
///
/// Service Contract
///
[ServiceContract(Namespace = "http://www.example.com/service")]
public interface ITestService
{
[OperationContract]
[WebInvoke(Method = "POST"
, ResponseFormat = WebMessageFormat.Json
, RequestFormat = WebMessageFormat.Json
, BodyStyle = WebMessageBodyStyle.WrappedRequest
, UriTemplate = "GetFoo.json/{name}?item={item}&count={countOfFoo}")]
FooDataContract[] GetFoo(string name, int item, int countOfFoo);
[OperationContract]
[WebInvoke(Method = "GET"
, ResponseFormat = WebMessageFormat.Json
, RequestFormat = WebMessageFormat.Json
, BodyStyle = WebMessageBodyStyle.WrappedRequest
, UriTemplate = "GetFooRaw.json")]
FooDataContract[] GetFooRaw();
}
///
/// Service Implementation
///
///
/// Each request will have its own instance of the service
///
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class TestService : ITestService
{
public FooDataContract[] GetFoo(string name, int item, int countOfFoo)
{
List result = null;
for (int i = 0; i ();
result.Add(new FooDataContract()
{
// default to "null"
Name = (name ?? "null") + "_" + i,
Age = age
});
}
return result == null ? null : result.ToArray();
}
public FooDataContract[] GetFooRaw()
{
List result = new List();
for (int i = 0; i < 5; i++)
result.Add(new FooDataContract() { Item = (i + 1) * 6, Name = "Test" + i.ToString() });
return result.ToArray();
}
}
The Android method that calls the WCF service is as followed
private void testSSLDataTransfer() throws ClientProtocolException, IOException, Exception
{
try {
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schemeRegistry.register(new Scheme("https", new EasySSLSocketFactory(getKeyStore(),"",getTrustStore()), 443)); //password is empty
HttpParams params = new BasicHttpParams();
params.setParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 1);
params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, new ConnPerRouteBean(1));
params.setParameter(HttpProtocolParams.USE_EXPECT_CONTINUE, false);
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, "utf8");
ClientConnectionManager clientConnectionManager = new ThreadSafeClientConnManager(params, schemeRegistry);
HttpContext context = new BasicHttpContext();
DefaultHttpClient client = new DefaultHttpClient(clientConnectionManager, params);
HttpPost post = new HttpPost("https://10.12.14.16:443/JsonTest/TestService.svc/json/GetFoo.json/Test?item=12&count=2");
HttpGet get = new HttpGet("https://10.12.14.16:443/JsonTest/TestService.svc/json/GetFooBar.json");
post.setHeader("Accept", "application/json");
post.setHeader("Content-type", "application/json");
post.setHeader("User-Agent", "android");
get.setHeader("Accept", "application/json");
get.setHeader("Content-type", "application/json");
get.setHeader("User-Agent", "android");
HttpResponse response = client.execute(get, context);
String statusLine = response.getStatusLine().toString(); //for debuf to see the response
HttpEntity responseEntity = response.getEntity();
InputStream stream = responseEntity.getContent();
InputStreamReader reader = new InputStreamReader(stream);
java.lang.StringBuffer stringBuffer = new java.lang.StringBuffer();
int read = 0;
while((read = reader.read()) >= 0)
stringBuffer.append((char)read);
String s = stringBuffer.toString();
stream.close();
} catch (ClientProtocolException e) {
throw e;
} catch (IOException e) {
String text = e.getMessage();
throw e;
} catch (Exception e) {
throw e;
}
}
The following part is used by the method testSSLDataTransfer to retrieve the client certificate keystore and the truststore
private KeyStore getKeyStore() throws IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException, UnrecoverableKeyException, Exception
{
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream in = this.getApplicationContext().getResources().openRawResource(R.raw.keystore);
try {
keystore.load(in, "changeit".toCharArray());
Key key = keystore.getKey("client", null); //It has no password and this way it finds the Key
}
catch (Exception e) {
throw e;
} finally {
in.close();
}
return keystore;
}
private KeyStore getTrustStore() throws IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException
{
KeyStore truststore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream in = this.getApplicationContext().getResources().openRawResource(R.raw.truststore);
try {
truststore.load(in, "changeit".toCharArray());
} finally {
in.close();
}
return truststore;
}
The EasySSLSocketFactory has been modified slightly so that the code looks like:
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyStore;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.scheme.LayeredSocketFactory;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* This socket factory will create ssl socket that accepts self signed
* certificate
*
* @author olamy
* @version $Id: EasySSLSocketFactory.java 765355 2009-04-15 20:59:07Z evenisse
* $
* @since 1.2.3
*/
public class EasySSLSocketFactory implements SocketFactory, LayeredSocketFactory {
private SSLContext sslcontext = null;
private KeyStore keystore = null;
private KeyStore truststore = null;
String keystorepassword = null;
public EasySSLSocketFactory()
{
}
public EasySSLSocketFactory(KeyStore keystore, String keystorepassword,KeyStore truststore)
{
this.keystore = keystore;
this.keystorepassword = keystorepassword;
this.truststore = truststore;
}
private static SSLContext createEasySSLContext(KeyStore keystore, String keystorepassword,KeyStore truststore) throws IOException {
try {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keystore, keystorepassword.toCharArray());
KeyManager[] list = keyManagerFactory.getKeyManagers();
SSLContext context = SSLContext.getInstance("TLS");
context.init(list, new TrustManager[] { new EasyX509TrustManager(truststore) }, null);
return context;
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
private SSLContext getSSLContext() throws IOException {
if (this.sslcontext == null) {
this.sslcontext = createEasySSLContext(keystore, keystorepassword, truststore);
}
return this.sslcontext;
}
/**
* @see org.apache.http.conn.scheme.SocketFactory#connectSocket(java.net.Socket,
* java.lang.String, int, java.net.InetAddress, int,
* org.apache.http.params.HttpParams)
*/
public Socket connectSocket(Socket sock, String host, int port,
InetAddress localAddress, int localPort, HttpParams params)
throws IOException, UnknownHostException, ConnectTimeoutException {
int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
int soTimeout = HttpConnectionParams.getSoTimeout(params);
InetSocketAddress remoteAddress = new InetSocketAddress(host, port);
SSLSocket sslsock = (SSLSocket) ((sock != null) ? sock : createSocket());
if ((localAddress != null) || (localPort > 0)) {
// we need to bind explicitly
if (localPort < 0) {
localPort = 0; // indicates "any"
}
InetSocketAddress isa = new InetSocketAddress(localAddress,
localPort);
sslsock.bind(isa);
}
sslsock.connect(remoteAddress, connTimeout);
sslsock.setSoTimeout(soTimeout);
return sslsock;
}
/**
* @see org.apache.http.conn.scheme.SocketFactory#createSocket()
*/
public Socket createSocket() throws IOException {
return getSSLContext().getSocketFactory().createSocket();
}
/**
* @see org.apache.http.conn.scheme.SocketFactory#isSecure(java.net.Socket)
*/
public boolean isSecure(Socket socket) throws IllegalArgumentException {
return true;
}
/**
* @see org.apache.http.conn.scheme.LayeredSocketFactory#createSocket(java.net.Socket,
* java.lang.String, int, boolean)
*/
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
return getSSLContext().getSocketFactory().createSocket(socket, host, port,autoClose);
}
// -------------------------------------------------------------------
// javadoc in org.apache.http.conn.scheme.SocketFactory says :
// Both Object.equals() and Object.hashCode() must be overridden
// for the correct operation of some connection managers
// -------------------------------------------------------------------
public boolean equals(Object obj) {
return ((obj != null) && obj.getClass().equals(EasySSLSocketFactory.class));
}
public int hashCode() {
return EasySSLSocketFactory.class.hashCode();
}
}
Client certificates not supported by Android untill a future version. To have client certificates in an application one must implement code him/herself to have this send.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With