RIM Crypto API: Creating custom certificate status provider plug-ins

Architectural Overview

The RIM Over-the-Air Status API provides efficient and flexible access to over-the-air certificate status protocols (such as OCSP, Online Certificate Status Protocol, RFC 2560) from a wireless device. A key ingredient to this architecture is the proxy model it relies upon. Instead of having the device connect directly (via TCP connections, for example) to status responders on the Internet, a proxy is placed between the device and the status responders. The device sends the appropriate information to the proxy, which can then communicate with as many responders as necessary (simultaneously), and return the appropriate response back to the device. This effectively eliminates the unnecessary overhead the device would have to deal with in communicating directly with responders, and allows the proxy to return just the relevant information back to the device. This is all accomplished using a single-pass (request-response) protocol between the device and proxy, conserving wireless network bandwidth and improving overall response speed.

This architecture supports the concept of multiple provider plug-ins on both the device and the proxy. So, as new (or perhaps custom) protocols are developed, additional providers can be added to the framework and fully integrated with the status API on the device.

Currently, the proxy acts a non-trusted entity (i.e. its responses are not digitally signed). Thus the provider plug-ins are responsible for sending appropriate information back to the device so that responder signatures can be verified.

Device-Side Operation

When status is requested from the device, all registered providers are given access to the request. If at least one provider is capable of fetching status for that request, the framework packages the data into a status request and sends it to the proxy to be processed.

A provider on the device is created by extending the CertificateStatusProvider class and implementing the appropriate methods to encode a request and decode the associated response (as returned by a matching provider on the proxy). Given the status request for a particular certificate, it is the responsibility of the device provider to ensure that the necessary information is collected and packaged for transmission to the proxy. For example, most status protocols would require some identification of the certificate in question and its issuer (perhaps by distinguished name or a hash). The device provider would gather and package this information and any other parameters necessary to complete the status request (such as server names).

When the response is received from the proxy provider, the device provider must extract the appropriate fields, verify any signatures in the responderís response, and inform the status API of the new certificate statuses. The details of this operation are described later.

Proxy-Side Operation

On the proxy, certificate status provider plug-ins are created by extending the class ProxyStatusProvider and implementing the processStatusRequest method. In this method, the request data from the device should be extracted from the request, the appropriate status responders should be queried for status, and the response(s) should be collected to be sent back to the device. Additionally, the proxy provider must be able to parse the responderís response and determine the status of the certificates in question. The proxy status framework must be informed of the response status so that ranking rules can be applied to multiple responses if necessary.

When a request is received by the proxy framework, it is pulled apart and the data for the various providers in the request is collected. The framework then checks for the existence of the appropriate plug-ins on the proxy, and passes the data to the associated providers. All provider processing (as done in processStatusRequest) is done in parallel rather than sequentially.

Since multiple plug-ins are supported, there is the potential for having multiple responses for a given certificate. Thus the proxy makes use of the following simple ranking rules to ensure that for a given certificate, only one response is returned to the device:

Writing Status Provider Plug-ins

Several steps must be taken to implement matching status provider plug-ins for the device and proxy. The next few sections describe in detail how this is done.

Creating the Device Provider Class

All status providers must extend the class CertificateStatusProvider and implement the appropriate methods. For the sake of example, consider a provider called SimpleStatusProvider:

public class SimpleStatusProvider extends CertificateStatusProvider
{
    /**
     * Create the single instance of this provider.
     */
    public SimpleStatusProvider()
    {
        super( 0x3843b7eae1091a56L ); // hash of SimpleStatusProvider
    }
    

    /**
     * Determines if the specified cert chain is compatible with this provider.
     */
    protected boolean checkCompatibility( 
        Certificate[] certChain, 
        boolean checkEntireChain )
    {
        // . . . 
    }


    /**
     * Encode the necessary fields for a given certificate status request.
     */
    protected void encodeRequest(
        Certificate[]           certChain, 
        boolean                 checkEntireChain, 
        ProviderRequestData     request, 
        KeyStore                keyStore, 
        ProviderUiContext       uiContext ) 
        throws StatusProviderException
    {
        // . . .
    }
    
    
    /**
     * Decode the response for a given certificate status request.
     */
    protected void decodeResponse( 
        Certificate[]           certChain, 
        boolean                 checkEntireChain, 
        ProviderResponseData    response, 
        KeyStore                keyStore, 
        ProviderUiContext       uiContext ) 
        throws StatusProviderException
    {
       // . . .
    }

 }

It is necessary to create a default constructor so that the provider can be instantiated (the constructor in the base class is protected). The base class constructor must be called from this default constructor, and the ID of the provider must be specified. The ID is a long that uniquely identifies the provider among other providers on the device and proxy. Thus, the value should be chosen in such a fashion as to avoid the possibility of a collision. A hash of a fully-qualified class name (including package) is probably best.

The only other methods that must be implemented are checkCompatibility, encodeRequest, and decodeResponse. checkCompatibility is used to determine if a given status provider is compatible with the specified certificate chain (correct certificate types, etc.) and can provide status for it. It should return true if the provider can comment on the chainís status.

encodeRequest encodes the information necessary for a given status query.

Its parameters are straightforward:

certChain This is the certificate chain as passed into the CertificateStatusRequest object. The first entry is the certificate in question, and additional entries (which may or may not be present) are the issuing CAs up to and possibly including a root certificate.
checkEntireChain A flag indicating if the status for the entire chain should be requested. This, of course, only makes sense of the rest of the chain is available.
request Encapsulates the request data fields that will be sent to the proxy for this provider (discussed later).
keyStore This parameter may reference a Key Store that can be used to search for additional certificates or build up the certificate chain if it is incomplete.
uiContext Allows providers to prompt the user and log error messages.

The certChain, checkEntireChain, and keyStore parameters are the same as those passed into the CertificateStatusRequest object for this status request. The ProviderRequestData parameter is of primary interest here as it is the mechanism for passing data to the proxy. Each provider is given its own ProviderRequestData object when encodeRequest is called.

There are several methods for adding data to this object:

void addGlobalField(int tag, byte[] value)

This method adds a global field to the request data for this provider. A global field is a piece of data that is considered to apply to the whole request. It may be, for example, the name of a responder to contact for certificate status. Each field is associated with a given integer tag. To access this same field on the proxy, one only needs to know its tag value. The data is simply dealt with as a byte array.

void addCertField(Certificate cert, int tag, byte[] value)

This method adds a data field associated with a particular certificate to the request data for this provider. When a field is added, it is associated with both a particular certificate and a particular integer tag. Thus, different certificates can have data fields with the same tag values. Once again, the data is dealt with as a byte array.

void setContextObject(Object context)

The method saves an object during the request process so it can once again be accessed when decodeResponse is called. The object is never sent to the proxy. Since only one global instance of a provider is ever created and used by the API, this provides a safe way to preserve state information.

The following code sample illustrates how all of this could be used to encode a certificate status request. For the sake of simplicity, it will be assumed that the status responder used needs the distinguished name of a certificateís subject and issuer to return status for it. The proxy will also need the URL of the responder.

protected void encodeRequest(
    Certificate[]           certChain, 
    boolean                 checkEntireChain, 
    ProviderRequestData     request, 
    KeyStore                keyStore, 
    ProviderUiContext       uiContext ) 
    throws StatusProviderException
{
    // The tags to identify various data fields
    final int TAG_RESPONDER_URL     = 1;
    final int TAG_CERT_SUBJECT      = 1;
    final int TAG_CERT_ISSUER       = 2;
    
    // We only support X509 certificates
    for( int i = 0; i < certChain.length; ++i ) {
        if( !( certChain[ i ] instanceof X509Certificate ) ) {
            uiContext.setErrorMessage( 
                "SimpleStatusProvider only supports X509 certificates" );
            throw new StatusProviderException();
        }
    }
        
    // Build up an incomplete cert chain if chain status is requested,
    // we have a key store, and the passed-in chain is incomplete
    if( checkEntireChain && certChain.length < 2 && keyStore != null ) {
        certChain = CertificateUtilities.buildCertChain( certChain[ 0 ],
                                                         keyStore );
    }
            
    // We will just be using one default responder URL
    request.addGlobalField( TAG_RESPONDER_URL, (
        "http://ors.simplestatusprovider.net" ).getBytes() );
    
    // For each certificate, encode the subject and issuer DN's
    for( int i = 0; i < certChain.length; ++i ) {
        X509Certificate cert = ( X509Certificate )certChain[ i ];
        
        request.addCertField( cert, TAG_CERT_SUBJECT,
                              cert.getSubject().getEncoding() );
        
        request.addCertField( cert, TAG_CERT_ISSUER,
                              cert.getIssuer().getEncoding() );
    }
}

The first section checks that all certificates in the request are X509 certificates. If any are not, an error message is logged and a StatusProviderException is thrown. This is the standard method for dealing with errors. Next, if the status of the entire chain is to be checked, the chain is incomplete, and a key store is specified, an attempt is made to complete the certificate chain. Finally, the global field for the responder is encoded and the data necessary for the individual certificates is added to the request.

Once the proxy has processed the request successfully, the response will be returned and decodeResponse will be called. The parameters are identical to those in encodeRequest except that a ProviderResponseData object rather than a ProviderRequestData object is given. The contents of ProviderResponseData are set on the proxy and are accessible on the device with the following methods:

byte[] getGlobalField(int tag)

Returns the global data field with the specified tag.

Enumeration getCertificates()

This method returns the certificates that are present in this response data. The device provider may only access or set the status of certificates found in this enumeration.

byte[] getCertField(Certificate cert, int tag)

Returns the certificate field associated with the specified certificate and tag value.

Object getContextObject()

Returns the context object initially set (on the device) while encoding the request.

void setCertificateStatus(Certificate cert, CertificateStatus status)

This method sets the status of the given certificate so that the framework can later call the listeners and possibly update the key store with this new status. If this method is not called for a certificate, the framework will consider the status unknown.

Using these methods, it is trivial to decode the response from the proxy and set the appropriate certificate statuses. In this example, the proxy returns the signed data and signature from the responder as global fields:

protected void decodeResponse( 
    Certificate[]           certChain, 
    boolean                 checkEntireChain, 
    ProviderResponseData    response, 
    KeyStore                keyStore, 
    ProviderUiContext       uiContext ) 
    throws StatusProviderException
{
    final int TAG_RESPONSE      = 1;
    final int TAG_SIGNATURE     = 2;
    
    // Get the response and its signature
    byte[] responseBytes = response.getGlobalField( TAG_RESPONSE );
    byte[] signature = response.getGlobalField( TAG_SIGNATURE );
    
    // Verify the response; prompt the user if it does not verify
    // The method verifyResponseSignature( ... ) is assumed to exist
    if( !verifyResponseSignature( responseBytes, signature ) ) {
        int code = uiContext.promptUser( "Warning", 
            "The status response's signature cannot be verified. " +
            "Do you still wish to accept the returned status?",
            new String[] { "Accept", "Cancel" },
            new int[] { 1, 2 } );
        switch( code ) {
            case 1:
                // Accept and continue
                break;
            case 2:
            case ProviderUiContext.NO_UI_AVAILABLE:
                uiContext.setErrorMessage( 
                    "The response signature could not be verified." );
                throw new StatusProviderException();
        }
    }
    
    // Set the status for each certificate in the response
    Enumeration certEnum = response.getCertificates();
    while( certEnum.hasMoreElements() ) {
        Certificate cert = ( Certificate )certEnum.nextElement();
        // Assume getResponseStatus( ... ) returns a CertificateStatus for the
        // given certificate based on the response data
        response.setCertificateStatus( 
            cert, getResponseStatus( responseBytes, cert ) );
    }
}

The responderís response and signature are extracted from the proxy response, and an attempt to verify the signature is made. If signature verification fails, the user is prompted and asked whether they still wish to accept the status. The promptUser method is useful if providers reach a state where user interaction is necessary. However, UI is only available if the status request was started as a UI request. Therefore, providers should always check the return value for NO_UI_AVAILABLE. In this case, no dialog is shown to the user.

After signature verification, the statuses for the certificates in the response are extracted. getCertificates is used to determine which certificates the proxy provider produced responses for, and setCertificateStatus is used to set the status of the given certificates. The status must be set on the device using this call for it to be recognized by the framework.

Creating the Proxy Provider Class

Creating a proxy status provider plug-in involves extending the class ProxyStatusProvider (located on the proxy itself) and implementing the appropriate methods. The following forms the skeleton for the proxy-side plug-in of our SimpleStatusProvider.

public class SimpleProxyStatusProvider extends ProxyStatusProvider
{
    /*
     * Create the single instance of the plug-in.
     */
    public SimpleProxyStatusProvider() 
    {
        super( 0x3843b7eae1091a56L );
    }
    

    /*
     * Process a status request and create the response.
     */
    public boolean processStatusRequest( 
        ProviderRequestData request, 
        ProviderResponseData response )
    {
        // . . .
        return true;
    }

}

Much like the device provider, a default constructor that calls the base constructor with the provider ID must be provided. The ID used must match that used on the device. The only other method that must be implemented is processStatusRequest, which takes both a ProviderRequestData and a ProviderResponseData object. All processing necessary to fetch the certificate status is done from this method.

The provider data interfaces are much like those on the device. Any data added to ProviderRequestData on the device becomes available in ProviderRequestData on the proxy with the following methods:

byte[] getGlobalField( int tag )

Returns the global data field identified by the given tag, as added by the device provider. The tag value for a particular field is the same as that used on the device.

int[] getCertIds()

Returns an array of integer IDs representing the certificates for which data was encoded on the device. When a certificate field is added to the device request data, an integer is associated with each certificate. This, if data for three different certificates were encoded on the device, this method would return three unique integer identifiers for those certificates.

byte[] getCertField( int certId, int tag )

Returns the certificate data field associated with the given certificate ID and tag value. The certificate ID should be one of the integers returned from getCertIds, and the tag is the same tag as used on the device.

Thus, the proxy ProviderRequestData interface is the mirror of that found on the device.

Any data added to the ProviderResponseData object is collected and sent back to the device to be accessed with its ProviderResponseData interface. ProviderResponseData has the following methods for adding data:

void addGlobalField( int tag, byte[] value )

Adds a global field with the given tag to the response.

void addCertField( int certId, int tag, byte[] value )

Adds a certificate data field associated with the given certificate ID and tag value to the response. Once again, this certificate ID must be one of the IDs returned by getCertIds. When the device receives the response, all of the certificate fields will be mapped back to the original certificates using these IDs.

void setCertStatus( int certId, int status )

Notifies the proxy framework of the status returned by this provider for the given certificate. This is necessary because raking rules are applied if more that one provider responds for a given certificate. The status integer must be one of the predefined status constants (STATUS_GOOD, STATUS_REVOKED, or STATUS_UNKNOWN).

Given these interfaces, it is possible to complete the sample status provider plug-in for the proxy (this code assumes that a hypothetical class OTARequest is available to open the connection and process and send the request to the status responder):

public boolean processStatusRequest( ProviderRequestData request, 
                                     ProviderResponseData response )
{
    // The field tags used for the device request
    final int TAG_RESPONDER_URL     = 1;
    final int TAG_CERT_SUBJECT      = 1;
    final int TAG_CERT_ISSUER       = 2;

    // Decode the URL for the responder and create the request object
    String url = new String( request.getGlobalField( TAG_RESPONDER_URL ) );
    OTARequest otaRequest = new OTARequest( url );

    // Add the data for all certificates
    int[] certIds = request.getCertIds();
    for( int i = 0; i < certIds.length; ++i ) {
        byte[] subject = request.getCertField( certIds[ i ], TAG_CERT_SUBJECT );
        byte[] issuer = request.getCertField( certIds[ i ], TAG_CERT_ISSUER );

        // Add the certificate to the request
        otaRequest.addCertificate( subject, issuer );
    }

    // Query the actual status responder
    if( !otaRequest.doQuery() ) {
        // We have an error
        return false;
    }

    // The tags used to extract the response and signature on the device
    final int TAG_RESPONSE          = 1;
    final int TAG_SIGNATURE         = 2;

    // Encode the response bytes and the signature
    response.addGlobalField( TAG_RESPONSE, otaRequest.getResponseBytes() );
    response.addGlobalField( TAG_SIGNATURE, otaRequest.getSignature() );

    // Set the status for each certificate we have status for
    for( int i = 0; i < certIds.length; ++i ) {
        byte[] subject = request.getCertField( certIds[ i ], TAG_CERT_SUBJECT );
        byte[] issuer  = request.getCertField( certIds[ i ], TAG_CERT_ISSUER );

        // Assume getStatus( ... ) returns the status for the given cert
        response.setCertStatus( 
            certIds[ i ], 
            otaRequest.getStatus( subject, issuer ) );
    }

    // Successful
    return true;
}

First, the OTARequest object using the URL sent from the device is created. Next, the subject and issuer for each certificate encoded on the device is extracted and added to otaRequest, providing the necessary information to retrieve status for the certificate identified by those fields. The request is then performed (it is assumed that doQuery opens the appropriate connections, sends the request bytes, waits for a reply, and receives and parses the response bytes.) If an error occurs in this process, the method immediately returns with false, indicating the operation was a failure. Finally, the response bytes and signature returned from the responder are added as global fields to the proxy response, and the status of each certificate in the response is passed to the framework with the setCertStatus method.

Registering Device and Proxy Providers

In order for these new providers to be recognized by the status API, they must be registered with both the device and proxy framework. On the device, this is accomplished by calling register in CertificateStatusProvider:

CertificateStatusProvider.register( new SimpleStatusProvider() );

On the proxy, the provider class (in .jar file or other form) must be placed into the extension directory, and the configuration file rimpublic.property must be updated to include the provider in the following configuration line:

application.handler.ocsp.StatusProviders = \ net.rim.statusproviders.SimpleProxyStatusProvider