Wednesday, May 6, 2009

JAX-WS webservice with client certificate auth

This post is not finished yet. But it will be eventually - have to log off now ( 06.may.2009 ) .

The goal


Running a JAX-WS SOAP webservice on Glassfish using client certificates for authentication. The client certificates will be issued by self appointed Certificate Authority (CA). We will develop sample clients for the webservice with Java and PHP (CURL). The CURL client should be easily portable to other platforms (Python, Perl, ...) as well.

The parts
1. Server key/certificate
2. CA key/certificate
3. Client keys/certificates
4. A JAX-WS webservice running on Glassfish V2.1 server
5. PHP CURL client
6. Java SOAP client

The process
1. User generates a private key and a Certificate request (CSR)
2. Provider signs the CSR and provides the user with a certificate
3. User connects to the webservice using his "client certificate" and private key

This article consists of 3 parts (each as a separate posting):
I. Part 1: Setting up the webservice
II. Part 2: The PHP-CURL client
III. Part 3: The Java client

Resources
1. OpenSSL Keytool Cheatsheet
2.

JAX-WS Part 2: PHP-CURL sample client

I'll keep the PHP code minimalistic and will not be creating a SOAP client with PHP. Instead I'll just post a ready-made SOAP xml using CURL and my client certificat/key pair. So the PHP code looks like this:


<?php

// connect to server with client cert and private key
if ($ch = curl_init()){

curl_setopt($ch, CURLOPT_URL, "https://localhost:8181/ExampleWS/Example");
curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 1);
curl_setopt($ch, 'CURLOPT_SSLCERTTYPE', 'PEM');
curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLCERT, 'democompany1.crt');
curl_setopt($ch, CURLOPT_SSLCERTPASSWD, 'changeit');
curl_setopt($ch, CURLOPT_SSLKEY, 'democompany1.key');
curl_setopt($ch, CURLOPT_SSLKEYPASSWD, 'changeit');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_CAINFO, NULL);
curl_setopt($ch, CURLOPT_CAPATH, NULL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml', 'SOAPAction: ""') );
curl_setopt($ch, CURLOPT_POST, 1);
$soap_message = <<<SOAP
<?xml version="1.0" encoding="utf-8" ?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Body>
<ns2:sampleEcho xmlns:ns2=""><sampleParamStr>Hello World!</sampleParamStr></ns2: sampleEcho >
</env:Body>
</env:Envelope>
SOAP;
curl_setopt($ch, CURLOPT_POSTFIELDS, $soap_message);
echo "Connecting\n";
$data = curl_exec($ch);
if ($data) {
echo "DATA: ".$data;
} else {
echo curl_error($ch);
}
echo "\nDone\n";

}


?>


You can run this with PHP-CLI

php curlsample.php

Tuesday, May 5, 2009

JAX-WS Part 1: setting up the webservice

Setting up CA and generating the CA cert
The CA is set up according to these instructions .

Generating the user key/CSR with OpenSSL
You can generate the CSR with this OpenSSL commands:

openssl req -new -nodes -keyout democompany1.key -out democompany1.csr -days 365

Generating the user key/CSR with keytool
When using a Java client for the webservice you probably want to use all the JSSE stack. So you are practically binded to the JKS keystore implementation. JKS keystore management is accomplished with "keytool" from command line. One keytool quirk is that you can not export/import private keys from/to a JKS keystore with the "keytool" command. So you must use Java code directly to manage your keys in a keystore or accept that restriction. Thus when using a Java client we need to generate our private key inside a JKS keystore from the beginning. You can generate the private key and the CSR with these keytool commands:

keytool -genkey -alias democompany2 -keyalg RSA -keystore democompany2_keystore.jks
keytool -certreq -alias democompany2 -keyalg RSA -file democompany2.csr -keystore democompany2_keystore.jks

Signing the user CSR
With the CA configured you can sign a CSR request with this command:

openssl ca -config openssl.my.cnf -policy policy_anything -out certs/democompany2.crt -infiles democompany2.csr

Configuring server: Glassfish domain.xml
The default Glassfish V2.1 server configuration does not have the truststore password properly configured. It is probably intentional since you should change the truststore pass for all the systems you run Glassfish on anyway. So to make things work you have to tell Glassfish it's truststore password by adding this to the JVM commands in domain.xml:

<jvm-options>-Djavax.net.ssl.trustStorePassword=changeit</jvm-options>

You also have to configure Glassfish auth realm so that it knows wich groups you want to authenticate with user certificates. So do this in your domain.xml :

(existing) <auth-realm classname="com.sun.enterprise.security.auth.realm.certificate.CertificateRealm" name="certificate">
(add this) <property name="assign-groups" value="TESTGROUP"/>
(existing) </auth-realm>

Configuring server: Glassfish truststore
You have to import the CA cert to your glassfish truststore so that the server will trust your signed certificates. You can do this with the keytool like this:

keytool -import -v -alias myCA -file certs/myca.crt -storepass changeit -keystore /Users/erkulas/apps/glassfish-v2.1-b60e/domains/domain1/config/cacerts.jks

Running server: an JAX-WS example webservice
This is a sample webservice configuration.
application.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE application PUBLIC "-//Sun Microsystems, Inc.//DTD J2EE Application 1.3//EN" "http://java.sun.com/dtd/application_1_3.dtd">
<application>
<display-name>security-Example-sslApp</display-name>
<module>
<web>
<web-uri>Esample-web.war</web-uri>
<context-root>ExampleWS</context-root>
</web>
</module>
</application>

sun-application.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sun-application PUBLIC "-//Sun Microsystems, Inc.//DTD Sun ONE Application Server 8.0 J2EE Application 1.4//EN" "http://www.sun.com/software/sunone/appserver/dtds/sun-application_1_4-0.dtd">
<sun-application>

<security-role-mapping>
<role-name>TESTGROUP</role-name>
<group-name>TESTGROUP</group-name>
</security-role-mapping>

</sun-application>

web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:j2ee="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.5"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

<servlet>

<servlet-name>Example</servlet-name>
<servlet-class>ws.Example</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Example</servlet-name>
<url-pattern>/Example</url-pattern>
</servlet-mapping>

<login-config>
<auth-method>TESTGROUP</auth-method>
</login-config>

<security-role>
<role-name>TESTGROUP</role-name>
</security-role>

<security-constraint>
<web-resource-collection>
<web-resource-name>Secure Area</web-resource-name>
<url-pattern>/</url-pattern>
<http-method>GET</http-method>

<http-method>POST</http-method>
<http-method>HEAD</http-method>
<http-method>PUT</http-method>
<http-method>OPTIONS</http-method>
<http-method>TRACE</http-method>
<http-method>DELETE</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>TESTGROUP</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>

</web-app>


ws.Example.java:

package ws;

import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.jws.WebParam;
import javax.xml.ws.WebServiceContext;

@javax.jws.WebService(targetNamespace = "")
public class Example {

@Resource
WebServiceContext wsContext;

@WebMethod
public String sampleEcho(@WebParam(name = "sampleParamStr", targetNamespace = "") String sampleParamStr ) throws java.lang.Exception
{
// sample of getting the principal (cert data) from messagecontext
MessageContext mx = wsContext.getMessageContext();
Subject subb = (Subject)( mx.get("CLIENT_SUBJECT") );
Object[] principals = subb.getPrincipals().toArray();
Principal principal = (Principal)principals[1];
String name = principal.getName();
// return input string as a result
ret = sampleParamStr;
return ret;
}
}