Monday, April 1, 2013

CXF security: getting certificates from central PKI

CXF security uses asymmetric algorithms for different purposes: encryption of symmetric keys and payloads, signing security token and messages, SSL transport bindings.

If you look in samples and tutorials, the public keys (in form of X509 certificates) are normally stored in java keystores.


For example, if sender encrypts the message payload sending to the receiver, he should have access to receiver certificate saved in local keystore. The sender uses this certificate for message encryption and receiver decrypts request with corresponded own private key:



Seems to be OK? Imagine now that you have production environment with 100 different clients of this service and service certificate is expired. You should reissue and replace certificate in ALL client keystores! Even more, if keystores are packaged into war files or OSGi bundles – they should be unpackaged and updated. Not really acceptable for enterprise environments.

Therefore large service landscapes have concept of central certificates management. It means that X509 certificates are not stored locally in keystores, but are provided and administrated centrally.

Normally it is a responsibility of PublicKey Infrastructure (PKI) established in organization. PKI is responsible for create, manage, store, distribute, synchronize and revoke public certificates and certification authorities (CAs).

W3C specifies standard SOAP interface to access and administrate keys remotely: XML Key Management Specification (XKMS 2.0). XKMS contains two parts:  
a) Key Information Service providing keys locating and validating functionality
b) Key Registration Service responsible for registration, revocation, recovery and reissuing of keys.

Note, that beginning from CXF 3.0.0, XKMS service and client implementation as well as XKMS Crypto Provider (see next chapter) will be available in CXF distribution.

WSS4J Crypto Providers
Fine, assume your organization has established simple PKI infrastructure and provides some kind of remote access to look up and validate the certificates (Rest or SOAP XKMS based).

The question now is the following: how to configure my CXF client and service to use public certificates not from local keystore, but from central PKI? Is there any easy way to do it?

Fortunately yes! CXF uses WSS4J for most of security aspects and WSS4J has own concept of crypto providers (don’t mix them with crypto providers of Java Cryptography Architecture).

WSS4J crypto providers are responsible to obtain X509 certificates and private keys, verify X509 trust chain and construct X509 certificates.

WSS4J crypto providers must implement Crypto interface. WSS4J also provides base abstract class with common functionality CryptoBase and keystore based implementation of crypto provider Merlin.

If you do not configure anything in CXF, WSS4J uses keystore based Merlin crypto provider by default. But, remember, we would like to get our certificates from PKI instead of keystore. Therefore it will be necessary to create own WSS4J crypto provider for this purpose. Interesting for us are two following methods:

  • boolean verifyTrust(java.security.cert.X509Certificate[] certs, boolean enableRevocation) 
Frist method looks up and returns X509 certificates based on CryptoType identifier. CryptoType specifies certificate identifier as subject DN, issuer DN and serial number, alias, thumbprint or SKI bytes.

We still obtain private keys from keystore, therefore corresponded methods will be delegated to standard Merlin provider. Skeleton implementation of PKI crypto provider can look like:


public class PKICryptoProvider extends CryptoBase {
    private Crypto defaultCrypto;
    private Properties keystoreProps;

    public PKICryptoProvider() {
        try {
            // load keystore properties and alias for private keys
            // …
            defaultCrypto = CryptoFactory.getInstance(keystoreProps);
        } catch (WSSecurityException e) {
            throw new IllegalStateException(
                    "Cannot instantiate default crypto provider: "
                            + e.getMessage(), e);
        }
    }

    @Override
    public X509Certificate[] getX509Certificates(CryptoType cryptoType)
            throws WSSecurityException {
        CryptoType.TYPE type = cryptoType.getType();
        X509Certificate[] certs = new X509Certificate[0];
        switch (type) {
        case SUBJECT_DN: {
            // get X509 certificate remotely from PKI on the base of subjectDN
            break;
        }
        case ALIAS: {
            // get X509 certificate remotely from PKI on the base of alias
            break;
        }
        case ISSUER_SERIAL: {
            // get X509 certificate remotely from PKI on the base of issuer DN
            // and serial number
            break;
        }
        default: {
            throw new IllegalArgumentException("Not supported cryptoType: "
                    + cryptoType);
        }
        }
        return certs;
    }

    @Override
    public String getX509Identifier(X509Certificate cert) {
        return cert.getSubjectDN().getName();
    }

    @Override
    public PrivateKey getPrivateKey(X509Certificate certificate,
            CallbackHandler callbackHandler) throws WSSecurityException {
        // …
        return defaultCrypto.getPrivateKey(identifier, password);
    }

    @Override
    public PrivateKey getPrivateKey(String identifier, String password)
            throws WSSecurityException {
        // …
        return defaultCrypto.getPrivateKey(identifier, password);
    }

    @Override
    public boolean verifyTrust(X509Certificate[] certs)
            throws WSSecurityException {
        return verifyTrust(certs, false);
    }

    @Override
    public boolean verifyTrust(X509Certificate[] certs, boolean enableRevocation)
            throws WSSecurityException {
        // call PKI remote method to validate certificate trust chain
        return false;
    }

    @Override
    public boolean verifyTrust(PublicKey publicKey) throws WSSecurityException {
        // call PKI remote method to validate certificate trust chain
        return false;
    }
}

As far as crypto provider makes a lot of remote calls, it makes sense to care about certificate caching.

Using PKI Crypto provider our message encryption picture will change in following way:







CXF distributions (starting from version 3.0.0) will contain XKMS based WSS4J Crypto implementation, so you can just reuse it.

Configure custom WSS4J Crypto Providers
How CXF and WSS4J know that they should use our custom crypto provider instead standard Merlin? There are some ways to configure it.

Keystore properties
You can specify own crypto provider in keystore properties file.
clientKeystore.properties:


org.apache.ws.security.crypto.provider=org.company.security.PKICryptoProvider
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=secret
org.apache.ws.security.crypto.merlin.keystore.private.password=secret
org.apache.ws.security.crypto.merlin.keystore.alias=myclient
org.apache.ws.security.crypto.merlin.keystore.file=./etc/keystores/clientstore.jks 
 
After it you configure keystore properties for CXF client or service as usual. For example in Spring it will be looks like:
 
<jaxws:client id="MyClient" xmlns:serviceNamespace="http://services.company.org/MyService"
    serviceClass="org.company.services.MyService" serviceName="serviceNamespace:MyServiceProvider"
    endpointName="serviceNamespace:MyServicePort" address="http://localhost:8080/services/MyService">
    <jaxws:properties>
        <entry key="ws-security.encyption.properties" value="clientKeystore.properties" />
        <entry key="ws-security.signature.properties" value="clientKeystore.properties" />
    </jaxws:properties>
</jaxws:client>

ws-security.encryption.properties says CXF that clientKeystore.properties will be used for the encryption. WSS4J will instantiate and call our PKICryptoProvider automatically. The same keystore properties can be specified for signature as well (ws-security.signature.properties).

Crypto provider object in Spring configuration
Sometimes more convenience way is to instantiate custom crypto provider explicitly. For example, if you want to inject proxy to communicate with PKI into PKICryptoProvider or use non-default constructor. For such cases CXF provides ws-security.encryption.crypto and ws-security.signature.crypto properties. You can pass pre-constructed PKICryptoProvider object using these properties.

Spring configuration in this case looks like:


<bean id="pkiCryptoProvider" class="org.company.security.PKICryptoProvider">
<!—-inject proxies, fields here -->
</bean>

<jaxws:client id="MyClient" xmlns:serviceNamespace="http://services.company.org/MyService"
    serviceClass="org.company.services.MyService" serviceName="serviceNamespace:MyServiceProvider"
    endpointName="serviceNamespace:MyServicePort" address="http://localhost:8080/services/MyService">
    <jaxws:properties>
        …
        <entry key="ws-security.encyption.crypto" value-ref=" pkiCryptoProvider " />
        <entry key="ws-security.signature.crypto" value-ref=" pkiCryptoProvider " />
    </jaxws:properties>
</jaxws:client>

Of course you can use the same approach to configure PKICryptoProvider for CXF service (jaxws:endpoint).

Specify Crypto provider object programmatically
Crypto provider object can be also passed programmatically. Client code will look like: 


PKICryptoProvider pkiCryptoProvider = new PKICryptoProvider();

SOAPService ss = new SOAPService(wsdlURL, SERVICE_NAME);
Greeter port = ss.getSoapPort();
Map<String , Object> requestContext =
    ((javax.xml.ws.BindingProvider)port).getRequestContext();
    requestContext.put(SecurityConstants.ENCRYPTION_CRYPTO,
    pkiCryptoProvider);
    requestContext.put(SecurityConstants.SIGNATURE_CRYPTO,
    pkiCryptoProvider); 
 
Remember that this code is not thread safe. To use thread safe request context it is necessary to add following property:

((BindingProvider)proxy).getRequestContext().put("thread.local.request.context", "true");

On the service side you can use this code:
…
@Resource
private WebServiceContext wsContext;
public ResponseData businessMethod(requestData) {
    PKICryptoProvider pkiCryptoProvider = new PKICryptoProvider();

    wsContext.getMessageContext().put(SecurityConstants.ENCRYPTION_CRYPTO, pkiCryptoProvider);
    wsContext.getMessageContext().put(SecurityConstants.SIGNATURE_CRYPTO, pkiCryptoProvider);
}

And in interceptor:

public class CustomSecurityInterceptor extends AbstractPhaseInterceptor<Message> {
    public CustomSecurityInterceptor () {
        super(Phase.PRE_LOGICAL);
    }

    @Override
    public void handleMessage(Message message) throws Fault {
       PKICryptoProvider pkiCryptoProvider = new PKICryptoProvider();

       message.put(SecurityConstants.ENCRYPTION_CRYPTO, pkiCryptoProvider);
       message.put(SecurityConstants.SIGNATURE_CRYPTO, pkiCryptoProvider);
}
 
Conclusion
In this blog we have discussed requirements and use cases to use central certificates management infrastructure, showed how to create custom PKI based crypto provider and configure it in CXF clients/services. PKI crypto provider can be used for communication with Security Token Service (STS) and inside STS to validate X509 certificates as well. But it is topic for the next blog.

4 comments:

  1. Hi
    Just wondering if you have published your next blog

    "PKI crypto provider can be used for communication with Security Token Service (STS) and inside STS to validate X509 certificates"

    Thanks
    Sangram

    ReplyDelete
  2. Hi Sangram,

    Not yet, but I will do it soon.
    Basically integration in STS is done through WSS4J Crypto as well. I would show how STS configuration will look like in this case.

    Regards,
    Andrei.

    ReplyDelete
  3. Hello and thanks a lot for the article.

    Is it normal to still have merlin properties in the properties file:

    "

    Keystore properties
    You can specify own crypto provider in keystore properties file.
    clientKeystore.properties:

    org.apache.ws.security.crypto.provider=org.company.security.PKICryptoProvider
    org.apache.ws.security.crypto.merlin.keystore.type=jks
    org.apache.ws.security.crypto.merlin.keystore.password=secret
    org.apache.ws.security.crypto.merlin.keystore.private.password=secret
    org.apache.ws.security.crypto.merlin.keystore.alias=myclient
    org.apache.ws.security.crypto.merlin.keystore.file=./etc/keystores/clientstore.jks


    "

    Thanks a lot.

    ReplyDelete
  4. And one more question:

    there is no configuration for the PKICryptoProvider?
    Fo example how does it know the address of the PKI server.

    Also, how do we pass the aliases we want? (either the alias for our certificate to sign with, or the one of some other web service to ecrypt when sending to that web service)

    Thanks.

    ReplyDelete