PicketLink STS - SSL Mutual Authentication

Version 4

    Clients traditionally identify themselves to the STS via username and password. There are cases, however, in which the active security policy requires all clients to use X.509 certificates when authenticating to the STS. In this document we describe the steps to enable SSL mutual authentication between clients and PicketLink STS on the JBoss Application Server. The configurations shown here are valid for JBoss AS versions 5.x and 6.x.

     

    1- Generating the certificates

    Before any configuration is done we must first generate the certificates that will be used by PicketLink STS and its clients and build the necessary keystores and truststores:

     

    • Generate the server (STS) private key and keystore

     

    keytool -genkey -alias server -keyalg RSA -keystore server.keystore -storepass serverpwd -keypass serverpwd
    

     

    This command generates the server.keystore file containing the server private key and associated public key certificate.

     

    • Export the server (STS) certificate

     

    keytool -export -alias server -keystore server.keystore -storepass serverpwd -file server.cer
    

     

    This command creates the server.cer file containing the exported server certificate. This is how this certificate looks like:

     

    $ keytool -printcert -file server.cer 
    Owner: CN=PicketLink STS, OU=Dev, O=JBoss, L=City, ST=State, C=US
    Issuer: CN=PicketLink STS, OU=Dev, O=JBoss, L=City, ST=State, C=US
    Serial number: 4e442e8b
    Valid from: Thu Aug 11 16:33:31 BRT 2011 until: Wed Nov 09 17:33:31 BRST 2011
    Certificate fingerprints:
         MD5:  A1:05:7E:EA:3E:B1:81:45:30:DB:81:42:CB:90:FA:DA
         SHA1: 0E:2D:F6:5E:C0:24:AC:B5:1D:E6:16:4D:F9:A7:4D:E8:23:FA:05:15
         Signature algorithm name: SHA1withRSA
         Version: 3
    

     

    • Create the client truststore and import the server certificate

     

    keytool -import -v -keystore client.truststore -storepass clientpwd -file server.cer
    

     

    This command creates the client.truststore file containing the trusted server certificate.

     

    • Generate the client private key and keystore

     

    keytool -genkey -alias admin -keyalg RSA -keystore client.keystore -storepass clientpwd -keypass clientpwd
    

     

    This command generates the client.keystore file containing the client's private key and associated public key certificate.

     

    • Export the client certificate

     

    keytool -export -alias admin -keystore client.keystore -storepass clientpwd -file client.cer
    

     

    This command creates the client.cer file containing the exported client certificate. This is how the client certificate looks like:

     

    Owner: CN=admin, OU=security, O=jboss, L=city, ST=state, C=us
    Issuer: CN=admin, OU=security, O=jboss, L=city, ST=state, C=us
    Serial number: 4e443ddb
    Valid from: Thu Aug 11 17:38:51 BRT 2011 until: Wed Nov 09 18:38:51 BRST 2011
    Certificate fingerprints:
         MD5:  84:7B:8F:D1:65:82:48:77:A9:37:F0:0C:6E:A9:46:46
         SHA1: BB:21:54:25:83:29:37:02:24:4A:FC:3C:74:08:A9:AC:84:D5:45:5E
         Signature algorithm name: SHA1withRSA
         Version: 3
    

     

    • Create the server truststore and import the client certificate

     

    keytool -import -v -keystore server.truststore -alias admin -file client.cer
    

     

    This command creates the server truststore file and adds the client public key certificate under the alias admin.

     

    2- Configuring JBoss Web

    Now that we have generated the certificates and associated keystores we must configure JBoss Web to enable the HTTPS connector. This is done by editing the server/default/conf/jbossweb.sar/server.xml file:

     

    <Connector protocol="HTTP/1.1" SSLEnabled="true" port="${jboss.web.https.port}" address="${jboss.bind.address}"
            scheme="https" secure="true" clientAuth="true" sslProtocol="TLS"
            keystoreFile="${jboss.server.home.dir}/conf/server.keystore"
            keystorePass="serverpwd"
            truststoreFile="${jboss.server.home.dir}/conf/server.truststore"
            truststorePass="serverpwd"/>
    

    As we can see, the configuration expects the server keystore and truststore files to be in the server/default/conf dir, so make sure you have copied the files to this location.

     

    Upon client authentication via SSL, JBoss Web creates a principal. By default, it uses the subject DN as the principal name. In our case, this means that the principal for the admin client would be CN=admin, OU=security, O=jboss, L=city, ST=state, C=us. This is the name that will appear in any token generated by PicketLink STS. In our example, we just want the subject CN to be used (i.e. admin) so we must tell JBoss Web to use a different principal implementation. This is done by replacing the certificatePrincipal attribute in the Realm configuration:

     

    <Realm className="org.jboss.web.tomcat.security.JBossWebRealm" 
            certificatePrincipal="org.jboss.security.auth.certs.SubjectCNMapping"
            allRolesMode="authOnly"/>
    

    The above configuration tells JBoss Web to use the SubjectCNMapping principal implementation. This class extracts the subject CN from the certificate and uses this CN as the principal name.

     

    3- Configuring PicketLink STS

    The next step is to configure PicketLink STS to use CLIENT-CERT authentication. To do this, we extracted the picketlink-sts.war that comes with the PicketLink distribution and changed the following files:

     

    • web.xml - set the login-config to CLIENT-CERT:

     

      <security-constraint>
         <web-resource-collection>
           <web-resource-name>PicketLinkSTSService</web-resource-name>
           <url-pattern>/*</url-pattern>
         </web-resource-collection>
         <auth-constraint>
           <role-name>JBossAdmin</role-name>
           <role-name>STSClient</role-name>
         </auth-constraint>
         <user-data-constraint>
           <transport-guarantee>CONFIDENTIAL</transport-guarantee>
         </user-data-constraint>
       </security-constraint>
    
       <login-config>
          <auth-method>CLIENT-CERT</auth-method>
          <realm-name>PicketLinkSTSRealm</realm-name>
       </login-config>
    

     

    • jboss-web.xml - specify the security domain used to authenticate and authorize requests:

     

    <?xml version="1.0" encoding="UTF-8"?>
    
    <jboss-web>
      <security-domain>picketlink-sts</security-domain>
    </jboss-web>
    

     

    • PicketLinkSTS.wsdl - change the web service address to reflect the HTTPS binding:

     

      <wsdl:service name="PicketLinkSTS">
        <wsdl:port name="PicketLinkSTSPort" binding="tns:STSBinding">
          <soap12:address location="https://localhost:8443/picketlink-sts"/>
        </wsdl:port>
      </wsdl:service>
    

     

    As we can see, the STS now expects to be authenticated and authorized by the picketlink-sts domain. This means we need to specify an application policy named picketlink-sts that has the login module configuration that will be used to authenticate and authorize requests. This is provided by the picketlink-sts-jboss-beans.xml file bellow:

     

    <?xml version="1.0" encoding="UTF-8"?>
    
    <deployment xmlns="urn:jboss:bean-deployer:2.0">
    
       <bean name="PicketLinkSecurityDomain" class="org.jboss.security.plugins.JaasSecurityDomain">
          <annotation>@org.jboss.aop.microcontainer.aspects.jmx.JMX(name="jboss.security:service=JaasSecurityDomain,domain=picketlink", exposedInterface=org.jboss.security.plugins.JaasSecurityDomainMBean)</annotation>
          <constructor>
             <parameter>picketlink</parameter>
          </constructor>
          <property name="keyStoreURL">${jboss.server.home.dir}/conf/server.truststore</property>
          <property name="keyStorePass">serverpwd</property>
       </bean>
    
       <application-policy xmlns="urn:jboss:security-beans:1.0" name="picketlink-sts">
           <authentication>
               <login-module code="org.jboss.security.auth.spi.CertRolesLoginModule" flag="required">
                   <module-option name="unauthenticatedIdentity">anonymous</module-option>
                   <module-option name="securityDomain">java:/jaas/picketlink</module-option>
                   <module-option name="rolesProperties">picketlink-sts-roles.properties</module-option>
               </login-module>
           </authentication>
       </application-policy>
    
    </deployment>
    

     

    In this case we are using the CertRolesLoginModule. This module uses a JaasSecurityDomain to check if the client certificate is included in the server keystore and extracts the roles from a properties file.

     

    NOTE: our JaasSecurityDomain specifies the server.truststore in the keyStoreURL parameter. This is due to a bug in the BaseCertLoginModule that checks the configured keystore when validating client certs when it should be using the trustStoreURL for that.

     

    The picketlink-sts-roles properties looks like this:

     

    admin=JBossAdmin,STSClient
    

     

    To deploy the STS, we must copy the following files:

    • picketlink-sts.war, picketlink-fed.jar and picketlink-sts-jboss-beans.xml to server/default/deploy
    • picketlink-sts-roles.properties to server/default/conf

     

    4- Sample Client Application

    The last step involves calling the STS from a remote client using mutual SSL authentication. The following class demonstrates how it is done:

     

     * JBoss, Home of Professional Open Source Copyright 2009, Red Hat Middleware
    package test;
    
    import javax.xml.transform.Result;
    import javax.xml.transform.Source;
    import javax.xml.transform.Transformer;
    import javax.xml.transform.TransformerFactory;
    import javax.xml.transform.dom.DOMSource;
    import javax.xml.transform.stream.StreamResult;
    
    import org.picketlink.identity.federation.api.wstrust.WSTrustClient;
    import org.picketlink.identity.federation.api.wstrust.WSTrustClient.SecurityInfo;
    import org.picketlink.identity.federation.core.wstrust.WSTrustException;
    import org.picketlink.identity.federation.core.wstrust.plugins.saml.SAMLUtil;
    import org.w3c.dom.Element;
    
    /**
     * <p>
     * This class shows how to use the {@code WSTrustClient} API to obtain and validate tokens with the PicketLink STS.
     * </p>
     * 
     * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
     */
    public class WSTrustClientTest
    {
       public static void main(String[] args) throws Exception
       {
          System.setProperty("javax.net.ssl.keyStore", "conf/client.keystore");
          System.setProperty("javax.net.ssl.keyStorePassword", "clientpwd");
          System.setProperty("javax.net.ssl.trustStore", "conf/client.truststore");
          System.setProperty("javax.net.ssl.trustStorePassword", "clientpwd");
    
          javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(
                new javax.net.ssl.HostnameVerifier(){
    
                    public boolean verify(String hostname, javax.net.ssl.SSLSession sslSession) {
                        if (hostname.equals("localhost")) {
                            return true;
                        }
                        return false;
                    }
                });
    
          new WSTrustClientTest().testSTS();
       }
    
       public void testSTS() throws Exception
       {
          // create a WSTrustClient instance.
          WSTrustClient client = new WSTrustClient("PicketLinkSTS", "PicketLinkSTSPort", 
                new String[]{
                   "https://localhost:8343/picketlink-sts/PicketLinkSTS", 
                   "https://localhost:8443/picketlink-sts/PicketLinkSTS"
                }, 
                new SecurityInfo(null, (String)null));
    
          // issue a SAML assertion using the client API.
          Element assertion = null;
          try 
          {
             assertion = client.issueToken(SAMLUtil.SAML2_TOKEN_TYPE);
          }
          catch (WSTrustException wse)
          {
             System.out.println("Unable to issue assertion: " + wse.getMessage());
             wse.printStackTrace();
             System.exit(1);
          }
    
          // print the assertion for demonstration purposes.
          System.out.println("\nSuccessfully issued a standard SAMLV2.0 Assertion!");
          printAssertion(assertion);
    
          // validate the received SAML assertion.
          try
          {
             System.out.println("\n\nIs assertion valid? " + client.validateToken(assertion));
          }
          catch (WSTrustException wse)
          {
             System.out.println("\n\nFailed to validate SAMLV2.0 Assertion: " + wse.getMessage());
             wse.printStackTrace();
          }
       }
    
       private void printAssertion(Element assertion) throws Exception
       {
          TransformerFactory tranFactory = TransformerFactory.newInstance();
          Transformer aTransformer = tranFactory.newTransformer();
          Source src = new DOMSource(assertion);
          Result dest = new StreamResult(System.out);
          aTransformer.transform(src, dest);
       }
    }
    

     

    • Required libs in classpath: picketlink-fed.jar and log4j.jar.
    • Application assumes the client keystore and truststore files to be in the conf/ dir.
    • Note that we had to override the default HostnameVerifier in javax.net.ssl. This is because the default verifier expects the CN of the server to match the server host address. In our case, the server CN is PicketLink STS while the address is localhost, which would lead to validation errors from the default verifier.
    • The WSTrustClient doesn't accept a null SecurityInfo instance, so we had to build an empty one even though no username or password is going to be used.

     

    If everything has been correctly configured, we should see the following output in the client application:

     

    Successfully issued a standard SAMLV2.0 Assertion!
    <?xml version="1.0" encoding="UTF-8"?><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="ID_f232b9cb-35b3-4b31-bc59-26a040f663f3" IssueInstant="2011-08-11T22:55:52.675Z" Version="2.0"><Issuer>PicketLinkSTS</Issuer><dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"><dsig:SignedInfo><dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#WithComments"/><dsig:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><dsig:Reference URI="#ID_f232b9cb-35b3-4b31-bc59-26a040f663f3"><dsig:Transforms><dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></dsig:Transforms><dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><dsig:DigestValue>oF/Xfw5ifEQhYlvrLX/o4by7+aw=</dsig:DigestValue></dsig:Reference></dsig:SignedInfo><dsig:SignatureValue>QjfaeAWYy5IALxOUbynFIywfiuMQ/Cvz0tBpVicn4+/5PGZ/Fyv+PdFjSqaSJLfwsaYkKLN31nQJ
    O/bYmttgTrEp+Y+KePKRCn8cHq9Dy5ktYHVHFFCISfpArrYIpCTcrS2Z1mbYAlqNv1pbcfwWBjmb
    C61IGCGT6bqmv5hVsOs=</dsig:SignatureValue><dsig:KeyInfo><dsig:KeyValue><dsig:RSAKeyValue><dsig:Modulus>suGIyhVTbFvDwZdx8Av62zmP+aGOlsBN8WUE3eEEcDtOIZgO78SImMQGwB2C0eIVMhiLRzVPqoW1
    dCPAveTm653zHOmubaps1fY0lLJDSZbTbhjeYhoQmmaBro/tDpVw5lKJns2qVnMuRK19ju2dxpKw
    lYGGtrP5VQv00dfNPbs=</dsig:Modulus><dsig:Exponent>AQAB</dsig:Exponent></dsig:RSAKeyValue></dsig:KeyValue></dsig:KeyInfo></dsig:Signature><saml:Subject><saml:NameID NameQualifier="urn:picketlink:identity-federation">admin</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/></saml:Subject><saml:Conditions NotBefore="2011-08-11T22:55:52.675Z" NotOnOrAfter="2011-08-12T00:55:52.675Z"/></saml:Assertion>
    
    Is assertion valid? true
    

     

    Notice the assertion subject name: admin. This is due to the SubjectCNMapping configured in the JBoss Web Realm. The default mapping would result in the whole DN being used as the subject name.