Páginas

viernes, 22 de marzo de 2013

Validacion de certificados mediante OCSP en Java

Introducción.

En esta entrada vamos a mostrar como realizar una petición a un servidor OCSP para determinar el estado de revocación de un certificado.

En el ejemplo se han usado las librerías Bouncy Castle (JDK 1.5 - JDK 1.7) disponibles en los repositorios MAVEN.

Dependencias.

Las dependencias del proyecto son:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>es.ine.sgtic</groupId>
  <artifactId>cliente-ocsp</artifactId>
  <version>1.0</version>
  <dependencies>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcprov-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcprov-ext-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcpkix-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcmail-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcpg-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
  </dependencies>
</project>


Codigo.

El código fuente del ejemplo es el siguiente:

package es.ine.sgtic;

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;

public class TestOCSP {

//    private static final String URL_OCSP_RESPONDER = "http://apus.cert.fnmt.es/appsUsuario/ocsp/OcspResponder"; 

//    private static final String URL_OCSP_RESPONDER = "http://ocspape.cert.fnmt.es/ocspape/OcspResponder";
//    private static final String URL_OCSP_RESPONDER = "http://ocsp.dnie.es";
    private static final String URL_OCSP_RESPONDER = "http://ocsp.dnielectronico.es/";
  
    public static void main (String args[]) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, OCSPException, OperatorCreationException{

        //Cargamos el almacen de certificados que contiene el certificado a verificar
        final InputStream fis = TestOCSP.class.getClassLoader().getResourceAsStream("almacen.p12");
        final KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(fis,"passwordAlmacen".toCharArray());

        //Sacamos el certificado del que necesitamos conocer su estado de revocacion
        final X509Certificate certAnalizable = (X509Certificate)ks.getCertificate("
aliasCert");

        //Sacamos el certificado raíz de la cadena de certificación
        final Certificate[] chain = ks.getCertificateChain("
aliasCert");
        final X509Certificate certRaiz = (X509Certificate)chain[1];

        //Se carga el proveedor necesario para la petición OCSP
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

        //Generamos el ID del cetificado que estamos buscando
        final CertificateID id = new CertificateID(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build().get(CertificateID.HASH_SHA1), new X509CertificateHolder(certRaiz.getEncoded()), certAnalizable.getSerialNumber());

        //Instanciamos el generador de requestOCSP
        final OCSPReqBuilder ocspPeticionBuilder = new OCSPReqBuilder();
        ocspPeticionBuilder.addRequest(id);
//
//        //create details for nonce extension
//        final BigInteger nonce = BigInteger.valueOf(System.currentTimeMillis());
//        final Extension ext = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, true, new DEROctetString(nonce.toByteArray()));
//        ocspPeticionBuilder.setRequestExtensions(new Extensions(new Extension[] { ext }));

        //Se genera la petición con el certificado a verificar y su número de serie
        final OCSPReq ocspPeticion = ocspPeticionBuilder.build();

        //Se establece la conexión HTTP con el ocsp del DNIe
        final URL url = new URL(URL_OCSP_RESPONDER);
        final HttpURLConnection con = (HttpURLConnection)url.openConnection();

        //Se configuran las propiedades de la petición HTTP
        con.setRequestProperty("Content-Type", "application/ocsp-request");
        con.setRequestProperty("Accept", "application/ocsp-response");
        con.setDoOutput(true);
        final OutputStream out = con.getOutputStream();
        final DataOutputStream dataOut = new DataOutputStream(new BufferedOutputStream(out));

        //Se obtiene la respuesta del servidos OCSP del DNIe
        dataOut.write(ocspPeticion.getEncoded());
        dataOut.flush();
        dataOut.close();

        //Se parsea la respuesta y se obtiene el estado del certificado retornado por el OCSP
        final InputStream in = (InputStream)con.getContent();
        final OCSPResp ocspRespuesta = new OCSPResp(in);
        int estado = ocspRespuesta.getStatus();

        //Analizamos la respuesta
        if (estado==OCSPResp.SUCCESSFUL) {
            System.out.println("OCSP-RESPONDER: PETICION TRATADA CORRECTAMENTE");
            System.out.println(tratarRespuestaOK(ocspRespuesta));
        } else if (estado==OCSPResp.MALFORMED_REQUEST) {
            System.out.println("OCSP-RESPONDER: ERROR. PETICION MAL FORMADA");
        } else if (estado==OCSPResp.INTERNAL_ERROR) {
            System.out.println("OCSP-RESPONDER: ERROR. ERROR INTERNO DEL SERVIDOR");
        } else if (estado==OCSPResp.TRY_LATER) {
            System.out.println("OCSP-RESPONDER: ERROR. REPETIR LA PETICION MAS TARDE");
        } else if (estado==OCSPResp.SIG_REQUIRED) {
            System.out.println("OCSP-RESPONDER: ERROR. PETICION DEBE SER FIRMADA");
        } else if (estado==OCSPResp.UNAUTHORIZED) {
            System.out.println("OCSP-RESPONDER: ERROR. PETICION OCSP NO ESTA AUTORIZADA");
        } else {
            System.out.println("OCSP-RESPONDER: ERROR. TIPO DE RESPUESTA DESCONOCIDO");
        }
    }

    private static String tratarRespuestaOK(final OCSPResp ocspRespuesta) throws OCSPException, IOException {

        //TODO:Afinar este método
      
        final Object responseObject = ocspRespuesta.getResponseObject();
        final BasicOCSPResp basicOCSPResp = (BasicOCSPResp) responseObject;
      
        final SingleResp[] responses = basicOCSPResp.getResponses();
        if (responses.length == 1) {
            final SingleResp resp = responses[0];
            final Object status = resp.getCertStatus();
            if (status == CertificateStatus.GOOD) {
                return "ESTADO DEL CERTIFICADO: VALIDO";
            }
            else if (status instanceof org.bouncycastle.ocsp.RevokedStatus) {
                return "ESTADO DEL CERTIFICADO: REVOCADO";
            }
            else {
                return "ESTADO DEL CERTIFICADO: DESCONOCIDO";
            }
        }
        return "VARIAS RESPUESTAS. AFINAR EL CODIGO";
    }
}


Alternativas.

Una posible alternativa a picar código en Java es usar openssl.

Las peticiones a los cuatro servidores OCSP indicados en el código equivaldrían a:

openssl ocsp -url http://apus.cert.fnmt.es/appsUsuario/ocsp/OcspResponder -resp_text -issuer FNMT.pem -cert aliasCert.pem
openssl ocsp -url http://ocspape.cert.fnmt.es/ocspape/OcspResponder -resp_text -issuer FNMT.pem -cert
aliasCert.pem
openssl ocsp -url http://ocsp.dnie.es -resp_text -issuer FNMT.pem -cert aliasCert.pem
openssl ocsp -url http://ocsp.dnielectronico.es/ -resp_text -issuer FNMT.pem -cert aliasCert.pem

Pendiente.

Sería interesante realizar las pruebas con un certificado válido, uno revocado y uno caducado (expirado) para analizar a fondo la respuesta contenida en el objeto BasicOCSPResp. En las pruebas, con un certificado expirado tenemos la salida:

OCSP-RESPONDER: PETICION TRATADA CORRECTAMENTE
ESTADO DEL CERTIFICADO: DESCONOCIDO


Hay que profundizar también en el mecanismo para firmar la petición, dado que por ejemplo el servidor: http://apus.cert.fnmt.es/appsUsuario/ocsp/OcspResponder pide la petición firmada.

Por último conviene aclarar si cualquier servidor OCSP ofrece información sobre cualquier certificado o solo por los emitidos por ciertas CA, de ahí el exceso de respuestas con: "ESTADO DEL CERTIFICADO: DESCONOCIDO".

Enlaces de interes:

http://tools.ietf.org/html/rfc2560
http://www.bouncycastle.org/latest_releases.html

2 comentarios:

  1. Hola.

    El artículo está muy bien, pero estoy teniendo problemas con la conexión cuando intento realizar la siguiente operación

    final DataOutputStream dataOut = new DataOutputStream(new BufferedOutputStream(out));

    en este caso me da el siguiente error:

    200 : OK
    true
    Exception in thread "main" java.net.ProtocolException: Cannot write output after reading input.

    sabes cual puede ser el motivo???

    ResponderEliminar
  2. Hola

    En la línea

    final OutputStream out = con.getOutputStream();

    Recogemos la respuesta que es un 200, pero que tiene que tener contenido que luego volcamos sobre ocspPeticion:

    final DataOutputStream dataOut = new DataOutputStream(new BufferedOutputStream(out));
    dataOut.write(ocspPeticion.getEncoded());

    Puede que no te esté llegando nada, si es así habla con la gente responsable del servicio OCSP para que te comenten porqué puede estar llegándote vacío

    Un saludo

    ResponderEliminar