| My Oracle & Java Blog

Tuesday, March 20, 2007

Using OCSP to validate client certificates with Oracle SSO

Oracle's documentation for using Single Sign On with Client Certificates has you use CRLs for checking for revoked certficates. A better method is to use OCSP (Online Certificate Status Protocol). OCSP is in improvement over CRLs, as it is basically a web service for validating certificate status. This has the advantage of leaving the management of the CRL up to the issuers, you don't have to maintain any lists of your own, and it provides real time validations. Oracle has provided built-in support for CRLs through it's Apache SSL & SSO modules. It has not, as of yet, provided the same for OCSP (there is no mod_ocsp module).

As a result, the only way to make an OCSP call to validate a certificate is to override the SSO authentication code. Oracle provides an interface that you can use to hook into Oracle's SSO code. Chapter 8 of the SSO Admin Guide discusses this in more detail.
First, the mapping module. The mapping module is called by the SSOX509CertAuth class. Normally, the mapping module would only take care of matching up the user's certificate to an account in OID, but we've also added the OCSP check here, since we required the mapping module in our implementation. You may not need the mapping module, especially if you store the user's full certificates in OID (we were not).
The alternative if you are not using the mapping moduleis to override the default MediumHigh authentication plugin (specified in policy.properties), with your own class. This can be done by extending Oracle's SSOServerAuth/SSOX509CertAuth class, or by implementing the IPASAuthInterface interface, and including the OCSP check directly in that class.

The Mapping Module Code:


public class CustomMappingModule implements IPASUserMappingInterface
{
private static final String X509_CERT_CLASS = "javax.servlet.request.X509Certificate";
private static final Logger logger = Logger.getLogger(CustomMappingModule.class.getName());

private OCSPValidator ocspValidator = new OCSPValidator();


public CustomMappingModule()
{

}

public IPASUserInfo getUserInfo(HttpServletRequest request) throws IPASException
{

try
{
IPASUserInfo l_usrInfo = null;
logger.fine("entering CustomMappingModule.getUserInfo");
//get the user's cert off the request
Object o = request.getAttribute(X509_CERT_CLASS);

X509Certificate[] l_usrBCerts = (X509Certificate[])request.getAttribute(X509_CERT_CLASS);

if( (l_usrBCerts != null) && ( l_usrBCerts.length >= 0) )
{
logger.fine("certificate array is size:" +l_usrBCerts.length);

//Make the OCSP call to check the cert status (throws exception if cert is bad)
if( ocspValidator.checkCertificate(l_usrBCerts[0]) ){

Principal l_usrPrincipal = l_usrBCerts[0].getSubjectDN();
String l_certDN = l_usrPrincipal.getName().toUpperCase();


// Set the realm name to null to use the default realm
String l_realm = null;

/*return the userInfo object, based on the CN value. here we call a function to strip the CN from the DN of the cert, but you can use this module to pull the CN from headers or other request attributes, etc*/
l_usrInfo = new IPASUserInfo(getCNfromDN(usrPrincipal.getName()) , l_realm);
}
}

if(l_usrInfo==null)
{
// User certificate not found.
logger.info("user info is null, throwing IPASException for user cert not found");
throw new IPASException("User certificate not found");
}
return l_usrInfo;
}
catch(Exception e)
{

e.printStackTrace();

throw new IPASException ("Error validating client certificate: "+ e.toString());
}

}

}


The mapping module above does two things:

  • Fetch the cert off the request, pulls out the CN value to return to SSO to map the account to log in with. Your implementation may be different, depending on how your certs match up with accounts in OID.

  • Calls OCSP to make sure the certificate provided is still valid (not revoked)


Now the code for the OCSPValidator class. This is a custom class, but I pulled most of the content almost verbatim from Oracle's OCSP sample code. However, I did streamline the code a bit, removing some aspects that were not required for my implementation, and added some comments along the way:



import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.logging.Level;
import java.util.logging.Logger;
import oracle.security.crypto.cert.X509;
import oracle.security.crypto.ocsp.BasicOCSPResponse;
import oracle.security.crypto.ocsp.HttpOCSPRequest;
import oracle.security.crypto.ocsp.OCSPContentHandlerFactory;
import oracle.security.crypto.ocsp.OCSPRequest;
import oracle.security.crypto.ocsp.OCSPResponse;
import oracle.security.crypto.ocsp.SingleBasicResponse;
import oracle.security.crypto.ocsp.SingleBasicResponse.CertStatus;
import oracle.security.crypto.ocsp.SingleRequest;
import oracle.security.sso.ias904.toolkit.IPASException;


public class OCSPValidator
{

private static final Logger logger = Logger.getLogger(OCSPValidator.class.getName());

public static final String OCSP_REPEATER_URL="http://www.myocspRepeater.com/

//statically initialize the class and set the content handler to the Oracle OCSP class
static
{
URLConnection.setContentHandlerFactory(new OCSPContentHandlerFactory());
}

public OCSPValidator()
{
}


/**
* This makes the OCSP call to check the certificate status. Returns true
* if it is valid. Throws an exception if it is invalid.
* @throws oracle.security.sso.ias904.toolkit.IPASException
* @return
* @param cert
*/
public boolean checkCertificate(X509Certificate cert) throws IPASException
{
try{

logger.fine("Creating Client Cert...");
X509 oraCert = convertCert(cert);
logger.fine("done:"+oraCert.getSubject().getCommonName());

logger.fine("Creating Issuer Cert...");
X509 issuerCert = getIssuerCert( cert.getIssuerDN().getName() );
logger.fine("done:"+issuerCert.getSubject().getCommonName());

URL url = new URL( OCSP_REPEATER_URL );

OCSPRequest req = new OCSPRequest();
Hashtable reqCerts = new Hashtable(); // CertID --> X509

logger.fine("Building HTTP request...");
SingleRequest singleReq = new SingleRequest(oraCert, issuerCert);
req.addRequest(singleReq);
reqCerts.put(singleReq.getCertID(), oraCert);

HttpOCSPRequest httpReq = new HttpOCSPRequest(req, url);
logger.fine("HTTP request created.");

OCSPResponse resp = httpReq.getResponse();
logger.fine("Status of the received response message: " + resp.getRespStatus());

if ((resp.getResponseInfo() != null) && (resp.getResponseInfo() instanceof BasicOCSPResponse))
{
BasicOCSPResponse basicResp = (BasicOCSPResponse)resp.getResponseInfo();

//I *beleive* this verifies the signiature on the response from the repeater (to make sure the
//repeater we are contating is valid
if (basicResp.getSigVerifyCerts() != null)
{
X509 certificateVerify = (X509)basicResp.getSigVerifyCerts().elementAt(0);
if (basicResp.verifySignature(certificateVerify.getPublicKey())){
logger.fine("Signature valid and verified with the certificate included in the response:\n" + certificateVerify.getSubject());
}else
{
//if the signiature is not valid and not verfied with the cert included, return false
logger.fine("Signature NOT valid with the certificate included in the response:\n" + certificateVerify.getSubject());
return false;
}
}

//next analyze the responses for the certificate status
//OCSP seems to math the subject DN on your cert, with what it has in it's
//repository, instead of matching with the Cert ID. As a result, multiple
//certificate status's are returned, so you must loop through the response
//and find the status for the cert ID that matches the one you sent
Enumeration e = basicResp.responses();
while (e.hasMoreElements() )
{
SingleBasicResponse singResp = (SingleBasicResponse)e.nextElement();
logger.fine("response serial no: "+singResp.getCertID().getSerialNo()+" Status: "+singResp.getCertStatus());
X509 c = (X509)reqCerts.get(singResp.getCertID());
//make sure the response matches the requested Cert ID, otherwise, keep looping until it does
if( c != null){
logger.info("Certificate Revocation Status");
logger.info("Certificate Name: "+ c.getSubject().getName());
logger.info("Certificate Issuer: "+c.getIssuer().getName());
logger.info("Certificate Serial Number: "+c.getSerialNo().toString());

logger.info(INFO,"Revocation Status: "+singResp.getCertStatus());

if(singResp.getCertStatus().equals( SingleBasicResponse.CertStatus.GOOD ))
{
return true;
}else if(singResp.getCertStatus().equals( SingleBasicResponse.CertStatus.REVOKED ))
{
return false;
}else if(singResp.getCertStatus().equals( SingleBasicResponse.CertStatus.UNKNOWN )){
return false;
}
}//end if
}//end while
}
}catch(Exception e)
{
//catch all errors here, log it, then throw the IPAS Exception so SSO handles it properly
logger.log(Level.SEVERE,"Error Mapping/Validating user certificate",e);
throw new IPASException("Unable to verify certificate status");
}
//if it gets here, the answer is false, not a valid cert, or unable to validate the cert
return false;
}

/**
* Retreive the Issuer's cert from the local keystore (oracle wallet).
*
* @throws java.io.IOException
* @throws java.security.GeneralSecurityException
* @return issuer's cert
* @param issuerDN
*/
public X509 getIssuerCert(String issuerDN) throws GeneralSecurityException, IOException
{
//the issuer DN on the CAC cards is coming back with spaces inbetween
//the compartments, after each comma, the aliases in the keystore
//do not have spaces after the comma, so I strip them out here
issuerDN = issuerDN.replaceAll(", ",",");

logger.fine("getting certificate for issuer: "+issuerDN);
X509Certificate x509Cert = null;
X509 oracleCert = null;

KeyStore keyStore = OracleKeyStore.getKeyStore();
//see if the root cert is in the keystore (think it has to be to get this far with Cert Auth)
x509Cert = (X509Certificate)keyStore.getCertificate(issuerDN);

return convertCert(x509Cert);
}

/**
* Convert the object class that is being used to hold the certificate from the
* JDK standard to X509Certificate, to the Oracle proprietary X509 object. The
* OCSP classes use the X509 object, but SSO provides the X509Certificate object
* on the request.
*
* @return
* @param cert
*/
public X509 convertCert(X509Certificate cert) throws GeneralSecurityException, IOException
{
X509 oraCert = null;

logger.fine("converting cert: " +cert.getSubjectDN().getName());
//get the encoded version of the certificate, X509 are ASN1 encoded
byte[] certBytes = cert.getEncoded();
logger.fine("Certificate Bytes length: "+ certBytes.length);
oraCert = new X509(certBytes);
logger.fine("converted cert: " +oraCert.getHolder());

return oraCert;
}



This class does several things as well.

  • OCSP requires you to send both the client certificate, plus that client's root certificate (I'm not quite sure why that is). So what you must do in this case is find the root certificate in your local wallet, and pass that along with the OCSP request.

  • Because the Oracle OCSP classes use a proprietary X509 class, you must convert the standard java X509Certificate class into and Oracle X509 class.

  • I removed most of oracles code dealing with certificate extensions, as they were not required for my implementation. See their sample code for the original version dealing with extensions.

  • Once object instances of both certificates are created, this class builds the request then analyzes the response to check the status. Note: OCSP can return multiple status responses, even if you send a single certificate, like this code does. It looks like it returns a status for any certificate with a matching Subject DN. As a result, the code above loops through the response and finds the correct status based on the unique cert ID you sent. I'm not sure why OCSP doesn't just match on the Cert ID itself and return a single response.



This code references the OracleKeyStore class, which is a custom class I wrote to access the Oracle wallet. Supposedly the wallet file is a standard PKCS12 format, but the Sun keystore classes are unable to decrypt the contents, so you must use Oracle's PKI provider to open the wallet file. That sample code is in another post, which can be found here.