Tema 4: Firma Digital y Certificado Digital

4.1. Autentificación por Firma Digital

Una firma digital asocia un individuo con unos datos. Por ejemplo para garantizar que yo he escrito un determinado e-mail éste se firma digitalmente. Una firma digital se implementa como un digest (asociado al mensaje que se desea firmar) procesado por una clave privada (asociada al usuario que firma).

Cuando el destinatario recibe el mensaje y su firma digital asociada se procede en tres pasos. (1)Obtener el digest del mensaje. (2) Usar la clave pública del remitente para extraer el digest de la firma digital. (3) Si ambos digests son iguales entonces validar.

En JCA, el proceso de crear el digest, firmar, extraer el digest y verificar la firma se lleva a cabo por clases del package java.security.Signature:

getInstance() Obtiene una instancia de un objeto Signature especificando algoritmo y opcionalmente el proveedor. P.e. MD5WithRSA,SHA1WithRSA, MD5WithDSA,SHA1WithDSA
initSign() Inicializa un objeto Signature con una clave privada para firmarlo.
initVerify() Inicializa un objeto Signature con una clave pública para verificarlo.
update() Una vez que el objeto signature está inicializado se le pasan los datos que se quieren firmar o verificar como array de bytes.
sign() Devuelve los bytes de la firma digital para los datos pasados a través de update().
verify() Tomando como argumento los bytes de una firma digital, devuelve un valor booleano indicando si la firma asociada a los datos pasados a update() es correcta.

Por ejemplo, si hemos generado un par de claves RSA y lo hemos guardado en la variable parClaves y tenemos un array de bytes llamado datos que contiene el texto a firmar, seguiríamos el siguiente proceso para firmarlo:

// Obtener instancia del objeto Signature e inicializarlo con
// la clave privada para firmarlo
Signature firma = Signature.getInstance("MD5WithRSA");
firma.initSign(parClaves.getPrivate());

// Prepara la firma de los datos
firma.update(datos);

// Firmar los datos
byte[] bytesFirma = firma.sign();

// Mostrar en ASCII
System.out.println("\nFirma:\n" + new BASE64Encoder().encode(bytesFirma));

Si posteriormente queremos realizar la verificación deberemos inicializar el objeto Signature para verificación y recoger un valor booleano o bien una SignatureException cuando la verificación no tenga éxito:

firma.initVerify(parClaves.getPublic());

// Pasar los datos que fueron firmados
firma.update(datos);

// Verificar
boolean verificado = false;
try {
  verificado = firma.verify(bytesFirma);
} catch (SignatureException se) {
	  verificado = false;
  }

if (verificado) {
  System.out.println("\nFirma verificada.");
} else {
  System.out.println("\nFirma incorrecta.");
}

El ejemplo completo se encuentra en FirmaDigital.java.

4.2. Certificados Digitales: Contenidos

Uno de los problemas de las firmas digitales es que en el momento de la validación no se tiene garantía de que la clave pública que se utiliza sea la correcta. Los certificados digitales intentan añadir identidad a una clave pública para garantizar que el dueño de dicha clave es quien dice ser. En definitiva, un certificado digital consiste en la clave pública más información firmada por la clave privada de una tercera parte denominada autoridad certificadora (certificate authority o CA) como Verisign o Thawte.

Para codificar certificados en JDK se utiliza el estándar X.509. Hay tres versiones (v1, v2, v3) cada una de las cuales añade ciertas características sobre la anterior. Por ejemplo un certificado X.509v1 contiene la siguiente información:

Version Campo que indica la versión del certificado: V1, V2 o V3.
Serial Number Entero largo que es único para la CA que emite el certificado.
Signature Algorithm Algoritmo que la CA utilizó para firmar el certificado.
Validity Intervalo de validez entre las fechas from... y to....
Subject Indica, en formato X.500 a quién se el emite el certificado.
Key Clave pública del sujeto del certificado.
Signature Firma de la CA que habrá de ser verificada contra la clave pública de la CA.

4.3. Generando Certificados: keystores y keytool

En Java un keystore o almacén de claves es una colección de certificados y claves. El kestore por defecto se encuentra en el fichero $HOME/.keystore y contiene dos tipos de entradas: certificados y claves. Los certificados son en efecto certificados activos válidos. Las claves son privadas o bien simétricas y deben estar asociadas a certificados que contienen los sujetos que poseen dichas claves. No obstante, por razones de seguridad no es recomendable almacenar claves simétricas en el almacén. En cualquier caso el almacén usa passwords para proteger el acceso a las claves privadas.

La herramienta que maneja almacenes de claves y permite crear certificados es la aplicación keytool. Algunas de las opciones de esta aplicación son:

-certreq Crea una petición de certificado por ejemplo para obtener un certificado de una CA, como Verisign, que esté contemplada en esta aplicación.
-delete Borra una entrada del keystore.
-genkey Genera un par de claves para un certificado auto-firmado. Se puede especificar el algoritmo con -keyalg. Por ejemplo -keyalg RSA
-keyclone Copia una entrada en el almacén.
-keystore Especifica un fichero como almacén.
-printcert Muestra un certificado digital.
-selfcert Genera un certificado digital auto-firmado.
-storepasswd Cambia el password del almacén.
-export Exporta un certificado de un almacén codifidado en DER. Si queremos codificación BASE64 hay que añadir la opción -rfc.

Por ejemplo, con keytool -v -list listaremos las entradas del fichero .keystore. Si queremos añadir una entrada, identificada por un alias, haremos lo siguiente: keytool -genkey -alias test donde test es el alias. La aplicación nos irá pidiendo los datos para el certificado. El contenido del certificado será algo parecido a micertificado. Si queremos exportarlo a codificación DER haremos lo siguiente: keytool -export -alias test -file micertificado.cer.

4.4. Certificados en Java

Las clases para el manejo de certificados en Java pertenencen al paquete java.security.cert:

CertificateFactory Se utiliza para generar instancias de objetos CertificateFactory a través de getInstance().
Certificate Clase abstracta que encapsula un certificado. Los métodos más importantes de esta clase son: getPublicKey() (que devuelve la clave pública del sujeto del certificado) y verify() (que tomando como argumento la clave pública de la CA verifica la firma del certificado).
X509Certificate Otra clase que proporciona métodos para manejar certificados X.509.

Por ejemplo podemos leer un certificado *.cer de un fichero e imprimirlo con las siguientes instrucciones: (el código completo se encuentra en ImprimirCert.java)

CertificateFactory factoria = CertificateFactory.getInstance("X.509");

// Abrir el fichero
FileInputStream fis = new FileInputStream (args[0]);

// Generar certificado para el fichero
Certificate cert = factoria.generateCertificate(fis);
fis.close();

// Imprimir información
System.out.println(cert);

Si por el contrario queremos leer el certificado directamente del keystore debemos utilizar la clase java.security.KeyStore. El método load() de esta clase permite cargar el almacén si le pasamos el nombre del fichero de almacén (p.e. .keystore) y el password correcto. A continuación el método getCertificate() permite obtener el certificado con un alias concreto (el código completo se encuentra en ImprimirCertKS.java):

// Abrir el keystore
FileInputStream fIn = new FileInputStream(fich_keystore);
KeyStore keystore = KeyStore.getInstance("JKS");

// Cargar el keystore
keystore.load(fIn, password);

// Obtener el certificado
Certificate cert = keystore.getCertificate(alias);

// Mostrar el certificado
System.out.println(cert);

4.5. Crear Nuestra Propia CA

Supongamos que pretendemos constituirnos en CA para nuestra propia organización. Esto nos permite emitir nuestros propios certificados. Así, p.e., podremos emitir certificados de e-mail para que todos los miembros de nuestra organización puedan firmar sus correos electrónicos o bien crear certificados para validar el acceso de los clientes a nuestras bases de datos, etc.

Para ello podemos utilizar las clases del paquete sun.security.x509. Entre las más importantes tenemos las siguientes:

X509CertImpl Proporciona la implementación del certificado X.509. Usaremos esta clase para crear un nuevo certificado y lo firmaremos con el método sign().
X509CertInfo Encapsula los atributos del certificado X.509 y nos permite instanciar dichos atributos en el momeno de crearlo.
X500Name Nombre X500 (CN=Common name, OU=Organizational Unit, O=Organization, L=Location, ST=State, C=Country) del sujeto.
AlgorithmId Identificación del algoritmo criptográfico.
CertificateSubjectName Nombre del sujeto del certificado.
CertificateValidity Período de validez del certificado.
CertificateSerialNumber Número de serie del certificado. Único para una CA dada.
CertificateIssuerName Nombre x.500 del emisor del certificado (cuando es auto-firmado coincide con el sujeto).
CertificateAlgorithmID Identificador del algoritmo usado para firmar el certificado.

Lo primero es crear un certificado y una clave privada para la CA. Para ello haremos lo siguiente:

keytool -genkey -v -alias CA -keyalg RSA -keystore almacen

A continuación nos pediran información X.500 sobre la CA y nos preguntarán la clave de la CA. Tenemos pués el certificado y la clave de la CA.

El siguiente paso consiste en crear un certificado que queramos firmar con el certificado de la CA (encadenamiento de certificados):

keytool -genkey -v -alias miClave -keyalg RSA -keystore almacen

Y de nuevo contestaremos a las preguntas rellenando los atributos que deseemos. No obstante el certificado que obtenemos es auto-firmado. Lo interesante es reemplazarlo por uno firmado por la CA. Para ello usaremos el certificado y la clave privada de la CA. Una vez leido el almacén (ver el código completo en FirmarCertificado.java) seguiremos los pasos que se detallan a continuación:

1) Leer la clave privada y el certificado de la CA

PrivateKey clavePrivadaCA = (PrivateKey)keystore.getKey(aliasCA, passwordCA);
java.security.cert.Certificate certificadoCA = keystore.getCertificate(aliasCA);

2) Crear una implementación X.509 para el certificado del CA

byte[] codificado = certificadoCA.getEncoded();
X509CertImpl implementacionCA = new X509CertImpl(codificado);
X509CertInfo infoCA = (X509CertInfo)implementacionCA.get
   (X509CertImpl.NAME + "." + X509CertImpl.INFO);
X500Name emisorCA = (X500Name)infoCA.get
   (X509CertInfo.SUBJECT + "." + CertificateIssuerName.DN_NAME);

3) Leer la clave privada y el certificado a firmar.

java.security.cert.Certificate cert = keystore.getCertificate(aliasCert);
PrivateKey clavePrivada = (PrivateKey)keystore.getKey(aliasCert, passwordCert);

4) Crear de nuevo otra implementación X.509 para el certificado a firmar.

codificado = cert.getEncoded();
X509CertImpl implementacionCert = new X509CertImpl(codificado);
X509CertInfo infoCert = (X509CertInfo)implementacionCert.get
   (X509CertImpl.NAME + "." + X509CertImpl.INFO);

5) Especificar y almacenar el período de validez.

Date inicio = new Date();
Date fin = new Date(inicio.getTime() + VALIDEZ*24*60*60*1000L);
CertificateValidity intervalo = new CertificateValidity(inicio, fin);

6) Crear y almacenar un número de serie.

infoCert.set(X509CertInfo.SERIAL_NUMBER,
   new CertificateSerialNumber((int)(inicio.getTime()/1000)));

7) Poner como emisor a la CA.

infoCert.set(X509CertInfo.ISSUER +
 "." + CertificateSubjectName.DN_NAME, emisorCA);

8) Fijar el algoritmo.

AlgorithmId algoritmo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
infoCert.set(CertificateAlgorithmId.NAME + "." +
   CertificateAlgorithmId.ALGORITHM, algoritmo);

9) Crear el nuevo certificado a partir del info.

X509CertImpl nuevoCert = new X509CertImpl(infoCert);

10) Firmar el nuevo certificado que acabamos de crear.

nuevoCert.sign(clavePrivadaCA, ALG);

11) Almacenar en el keystore.

keystore.setKeyEntry(aliasNuevo, clavePrivada, passwordCert,
    new java.security.cert.Certificate[] { nuevoCert } );

12) Almacenar el keystore en el fichero.

FileOutputStream output = new FileOutputStream(fich_keystore);
keystore.store(output, password);
output.close();

Para aplicar esta firma ejecutamos el siguiente comando:

java FirmarCertificado almacen CA miClave miClave2

Así, habremos añadido un nuevo certificado, llamado miClave2, al almacén. Este certificado está emitido por nuestra CA (ver el emisor o issuer).Para verlo haremos lo siguiente:

keytool -list -v -keystore almacen

Finalmente, podemos exportar este certificado codificado en DER o en BASE-64 para usarlo. Sin embargo, en la práctica no se aceptará por defecto, a no ser que importemos el certificado de la CA y lo instalemos en un navegador. Para exportar el certificado de la CA a un fichero de modo que podamos cargarlo en un navegador haremos lo siguiente:

keytool -export -alias CA -keystore almacen -file CA.crt