Friday, 22 January 2010

Preparing for SCDJWS Part 17: Web Services transport level security

Share
Section 8 of SCDJWS exam objectives is dedicated to Web Services security.

Today I will talk about transport level security such as basic and mutual authentication, and SSL.

Theory

Basic authentication is the most basic authentication mechanism provided by HTTP protocol. Simply in HTTP headers there is one additional header containing user's credentials.

But it is very easy to decode username and password. Thus basic authentication in most often used with SSL to provide confidentiality.

SSL/TLS is used in HTTPS.

Most commonly when server is accessed over HTTPS server authenticates itself with a certificate signed by a trusted authority.

In some cases where security is crucial aspect of the whole system architects may decide to enable mutual authentication. In this scenario not only server, but also client must present a valid certificate.

Practice - the Web Service

I implemented simple web-tier Web Service:
@WebService
public class HiHeyHelloWebService {
@Resource
private WebServiceContext context;
public String sayHiHeyHello(String name) {
Principal principal = context.getUserPrincipal();
return "Hi, hey, hello " + name + "! You have invoked me as '" + principal.getName() + "' and I think you are in admin role: " + context.isUserInRole("admin");
}
}
and deployed it on Apache Geronimo 2.1.3 (Tomcat based).

Web Service unit test

Using wsimport I created Web Service client. Then I wrote the stub of my IT case:
public class HiHeyHelloWebServiceITCase {
@Test
public void testSayHiHeyHello() {
HiHeyHelloWebServiceService service = new HiHeyHelloWebServiceService();
HiHeyHelloWebService port = service.getHiHeyHelloWebServicePort();
String response = port.sayHiHeyHello("Lukasz");
Assert.assertEquals("Hi, hey, hello Lukasz! You have invoked me as 'system' and I think you are in admin role: true", response);
}
}
Basic authentication

In web.xml I added the following <security-constraint />:
<security-constraint>
<web-resource-collection>
<web-resource-name>Protected</web-resource-name>
<url-pattern>/HiHeyHelloWebServiceService</url-pattern>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
<security-role>
<role-name>admin</role-name>
</security-role>
Note that your application server specific web descriptor has to be updated as well.

In my case, in geronimo-web.xml I used geronimo-admin server-wide security realm:
<security-realm-name>geronimo-admin</security-realm-name>
<security>
<default-principal>
<principal name="anonymous"
class="org.apache.geronimo.security.realm.providers.GeronimoUserPrincipal" />
</default-principal>
<role-mappings>
<role role-name="admin">
<principal name="admin" designated-run-as="true"
class="org.apache.geronimo.security.realm.providers.GeronimoGroupPrincipal" />
</role>
</role-mappings>
</security>
I packaged and redeployed the web app. Then I ran the test. It failed:
com.sun.xml.internal.ws.client.ClientTransportException: request requires HTTP authentication: Unauthorized
at com.sun.xml.internal.ws.transport.http.client.HttpClientTransport.checkResponseCode(HttpClientTransport.java:197)
at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.process(HttpTransportPipe.java:137)
at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.processRequest(HttpTransportPipe.java:74)
As expected.

Basic authentication was enabled and I had to supply valid username and passowrd to access the service.

I added the following two lines to my test (before the invocation of Web Service):
((BindingProvider) port).getRequestContext().put(BindingProvider.USERNAME_PROPERTY, "system");
((BindingProvider) port).getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, "manager");
And re-ran the test. It passed then.

Transport guarantee confidential

In web.xml as a child of <security-constraint /> element I added the following element:
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
I packaged and redeployed the web app.

When I re-ran the test I got the following exception:
javax.xml.ws.WebServiceException: No Content-type in the header!
at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.process(HttpTransportPipe.java:143)
at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.processRequest(HttpTransportPipe.java:74)
In tcpmon I saw that Geronimo returned 302 Moved Temporarly with a new https address of my Web Service. JAX-WS cannot handle HTTP redirects. That's not a problem, in Java code I added the following line:
((BindingProvider) port).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, "https://localhost/secure-web-service/HiHeyHelloWebServiceService");
I ran the test again. It failed because Geronimo's default certificate was not signed by trusted CA:
com.sun.xml.internal.ws.client.ClientTransportException: HTTP transport error: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at com.sun.xml.internal.ws.transport.http.client.HttpClientTransport.getOutput(HttpClientTransport.java:119)
at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.process(HttpTransportPipe.java:128)
at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.processRequest(HttpTransportPipe.java:74)
I had to generate my own trust CA and import Geronimo's certificate into it. Here's how I did it:
  1. I exported server's certificate:
    keytool -exportcert -alias geronimo -file server.cer -keystore %GERONIMO_HOME%\var\security\keystores\geronimo-default
  2. then I imported it into my own trust store:
    keytool -importcert -trustcacerts -alias geronimo -keystore ws_client_cacerts.jks -file server.cer
    As a password I set secretca.

  3. I added javax.net.ssl.trustStore properties to my IT case:
    System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/ws_client_cacerts.jks").getAbsolutePath());
    System.setProperty("javax.net.ssl.trustStorePassword", "secretca");
I re-ran the test and it passed.

Mutual authentication

In web.xml I changed authentication method to:
<auth-method>CLIENT-CERT</auth-method>
I packaged and redeployed the web application.

When I run the IT case, on the client side the exception was:
javax.xml.ws.WebServiceException: java.net.SocketException: Software caused connection abort: recv failed
at com.sun.xml.internal.ws.transport.http.client.HttpClientTransport.checkResponseCode(HttpClientTransport.java:223)
at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.process(HttpTransportPipe.java:137)
at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.processRequest(HttpTransportPipe.java:74)
On the server side the exception was:
10:12:18,146 WARN  [Http11Processor] Exception getting SSL attributes
javax.net.ssl.SSLHandshakeException: null cert chain
at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1591)
at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:187)
at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:177)
at com.sun.net.ssl.internal.ssl.ServerHandshaker.clientCertificate(ServerHandshaker.java:1206)
As expected. My client had to send its certificate to Geronimo.

By default Apache Geronimo does not have trust store CA. I know that Glassfish has one by default. But that's not a problem.

  1. I signed in into Geronimo's console, navigated to http://localhost/console/portal/Security/Certificate%20Authority and clicked Setup Certification Authority.

    It's a very simple 2 step "wizard", I entered some dummy data, on the confirmation screen I saw: CA Setup is successful!

  2. I navigated to http://localhost/console/portal/Server/Web%20Server selected TomcatWebSSLConnector and clicked edit. I found and set the following properties:

    truststoreFile
    - var/security/keystores/ca-keystore
    truststorePassword
    - secret (set previously in Setup Certification Authority!)

    I clicked Save button.

  3. Using command line I generated my keystore
    keytool -genkey -alias ws_client -keystore ws_client.jks
    As a password I set: secretks.

    Important here! The ws_client alias and keystore password must be the same! Otherwise you'll get the following error when connecting using SSL: UnrecoverableKeyException: Cannot recover key.
    It took me more than an hour to figure out what was happening.

  4. I exported client certificate
    keytool -exportcert -alias ws_client -keystore ws_client.jks -file ws_client.cer
  5. I imported client certificate into server's ca-keystore:
    keytool -importcert -alias ws_client -file ws_client.cer -keystore %GERONIMO_HOME%\var\security\keystores\ca-keystore
  6. and verified if was added
    keytool -list -keystore %GERONIMO_HOME%\var\security\keystores\ca-keystore
  7. I restarted Geronimo.

  8. In my IT case I added my key store:
    System.setProperty("javax.net.ssl.keyStore", new File("src/test/resources/ws_client.jks").getAbsolutePath());
    System.setProperty("javax.net.ssl.keyStorePassword", "secretks");
I re-ran my test.

I was almost successful... Almost becuase I the mutual authentication took place, SSL connection was established, but I got HTTP/1.1 401 Unauthorized response and the following exceptions:
10:57:40,926 WARN [TomcatGeronimoRealm] Login exception authenticating username
"CN=Lukasz Budnik,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=PL"
javax.security.auth.login.LoginException
root cause:
Caused by: javax.security.auth.callback.UnsupportedCallbackException: Wrong call
back type: class javax.security.auth.callback.NameCallback
at org.apache.geronimo.security.realm.providers.CertificateChainCallback
Handler.handle(CertificateChainCallbackHandler.java:67)
I asked people on users@geronimo.apache.org, but no one knew the answer. I also checked the Geronimo SVN repository and couldn't find unit test for mutual authentication.
So I guess in Geronimo you cannot have auth-constraint when using mutual authentication... :(

Summary

I'm quite disappointed that Geronimo doesn't support CLIENT-CERT authentication method...

Very similar to web tier, the EJB tier security works. Plus when using EJB tier security you can benefit from fine-grained class/method level security.

Next post - message level security: XML Signature, XML Encryption.

Stay tuned,
Łukasz

5 comments:

Tony said...

I was asked to create, with Netbeans (glassfish), Web service asynchrono (in Java) that communicates with a BPEL process content. Can someone help me? give me some examples?

thanks


sorry for my English, I am Italian

e.jfuhr. said...

I tried Mutual Certificate Security - out of the box - using Metro 2.0 and Tomcat. I wish they put the example together with maven. Check out the Metro 2.0 samples/ws-security/src/mcs (Mutual Certificate Security). Make some careful adjustments to the properties files and watch any initial errors in the wsdl files.
At least it works!!!

As for Tony, get Netbeans 6.5 with the SOA examples. They will probably get you started...

Anonymous said...

from the Geronimo documentation :

"In Geronimo 1.0, the first time a login module is called an UnsupportedCallbackException is thrown. This pass is just to establish which callbacks the login module requires, so the server can create a consolidated list of all the callbacks required by all login modules for the realm. Don't despair! The next time the login module is called it will be for real."

electronic signature in word said...

Really useful blog but I want to add my experience that is I got issued a certificate for my website say www.example.com it was working fine with this domain but if the site opens with url http://example.com i.e. without www then it gave security warnings.I was amazed that why Its behavior is like this

e.jfuhr. said...

i THINK the logic of "www.example.com" vs "example.com" goes something like this:
the server DNS number should be the same for both addresses. i.e., for localhost the DNS is "127.0.0.1". that translates to "localhost". for some reason, "www.example.com" and "example.com" do NOT translate to the same DNS server address. i hope that helps.
also note the example's "ENDPOINT_ADDRESS_PROPERTY" property. if set to "https://www.example.com/..." that is not the same as "https://example.com/..."