Cómo se comprueba la integridad de un programa en Java

miércoles, 4 de diciembre de 2013

Hace unos meses hablamos de cómo se comprobaba la firma digital de los ficheros ejecutables en Windows. Veamos en esta ocasión cómo funciona la firma digital de ficheros creados en Java, su estructura interna y cómo se realiza la comprobación paso a paso.

Keystore de CAs en Java
Los programas en Java pueden tomar forma de ficheros .jar, .apk... según se usen para ejecutables, applets, programas para Android... 

En realidad no son más que zips con un formato muy característico. Los ficheros de este tipo firmados, además, contienen en su interior una serie de archivos que permiten garantizar su integridad y, en cierta forma, su procedencia gracias a un certificado. Los certificados de los programas Java firmados, se validan en un "keyring" de CAs que viene de serie con la instalación de Java, y que es independiente al de Windows o del navegador Firefox, por ejemplo. Los applets, sin embargo, sí que se pueden "validar" contra el keystore de Windows.

En general, la comprobación de la firma se realiza "normalmente" con la misma aplicación del SDK de Java que permite firmarlos (jarsigner), solo que modificando la opción. Por ejemplo:

Ejemplo de validación de firma de un APK
Lo que ofrece información sobre ficheros, la verificación, etc... pero ciertamente es una información muy pobre y peor presentada visualmente. ¿Qué está ocurriendo internamente? En realidad, varias comprobaciones diferentes. Toda la información relativa a la firma e integridad se almacena dentro, en el directorio META-INF del ZIP. Ahí se pueden encontrar varios ficheros. Vayamos por partes.

Manifest.mf

Es un fichero donde se incluye el hash (normalmente SHA1) de cada fichero contenido en el ZIP. El hash se codifica en base64. El formato es simple: primero unas cabeceras, y luego "Name" y SHA1-Digest (que puede ser SHA256-Digest). Por ejemplo, AfPh3OJoypH966MludSW6f1RHg4= , decodificado en hexadecimal da como resultado 01f3e1dce268ca91fdeba325b9d496e9fd511e0e que es el SHA1 de ese archivo.

Abajo, en la imagen de la izquierda se observa un ejemplo real de manifiesto.

Un fichero Signature File (extensión SF)

También en el directorio META-INF, se encontrará un fichero con cualquier nombre, pero de extensión SF (signature file). En él se observan de nuevo unas cabeceras, y un formato muy parecido al del manifiesto, con lo que se diría que es el SHA1 de cada fichero... otra vez. Pero curiosamente, diferente al valor que se muestra en el manifiesto. En realidad, el hash que aparece el el signature file, no es el hash del fichero, sino de las tres líneas del manifiesto donde se muestra el hash. Si se almacenan tal cual las tres líneas (Name, SHA1-Digest y una en blanco) en un archivo de texto, se calcula su SHA1 y se codifica en base64, el resultado sería el que aparece en el signature file.

Fichero manifiesto a la izquierda y "signature file" a la derecha de un mismo APK.
En la cabecera del signature file, a través de SHA1-Digest-Manifest, se calcula al SHA1 del fichero del manifiesto. Efectivamente, en el ejemplo, Og+1qY7DjNHiykvwIOijpOUYPBI= coincide con el SHA1 en hexadecimal de archivo de manifiesto... Como el manifiesto a su vez, contiene el hash del resto de archivos, estamos de esta forma "validando" a todos los ficheros internos de una vez. Comprobando el hash del manifiesto (en el signature file), de alguna manera, comprobamos el resto de archivos. En esta imagen de ejemplo, se resume todo.



El certificado

Lo explicado hasta ahora cubre la integridad, pero no se ha hablado de la criptografía pública. ¿En qué punto entra el certificado? Finalmente, en el directorio META-INF se puede encontrar un fichero con extensión DSA o RSA, según se use uno u otro algoritmo de firma digital.

Lo que se firmará con la clave pública del certificado, es el hash del fichero "signature file". Así se "cierra el círculo". El manifiesto mantiene un listado de todos los ficheros internos y su integridad, el "signature file", mantiene la integridad del manifiesto. Y el signature file es firmado con una clave pública.

La verificación

La verificación se puede observar en el  propio código fuente de Java o OpenJDK o en su documentación. Resumiendo, se hace en varios pasos:

1) Se verifica la firma asímétrica del signature file (extensión .SF). Se verifica la firma RSA o DSA contra el certificado.

2) Si existe el valor "SHA1-Digest-Manifest" (u otro SHA) en el fichero SF, se comprueba que es el correcto. Con esto se asegura que el manifiesto no ha sido alterado. Si es correcto, se elude el paso siguiente (y se pasa al 4).

3) Si esa cabecera no existe, o su valor no es correcto (se asume que se ha modificado el hash del manifiesto), no se considera inválido el archivo jar o apk directamente. En realidad, se hacen dos cosas:
  • a) Si existe la cabecera Digest-Manifest-Main-Attributes en el fichero SF, se calcula el hash de los atributos principales del manifiesto. Los "atributos principales" son esos primeros datos que aparecen en el fichero y que no corresponden propiamente a cada archivo interno. Si no es válido, la comprobación falla.
  • b) Si no existe esa cabecera, se comprueba una a una cada entrada en el fichero SF. O sea, que el SHA1 corresponde a esas tres líneas del manifiesto. Con esto gana velocidad y eficiencia, puesto que no tiene que calcular el hash de cada fichero, sino que se fía de que el hash ya calculado en el momento de compilación y contenido en el manifiesto, no haya sido alterado. Si algo falla, la firma falla.
4) En el cuarto paso, se comprueba cada fichero, su hash real, contra el calculado en el manifiesto.

El modelo en general es bastante curioso. Se puede dar el caso en el que difiera el hash real del manifiesto con el  hash del manifiesto almacenado en el fichero de firmas... pero que eso no signifique que la firma no sea válida. Y esto es así porque se pueden añadir ficheros a un .jar, sin que se altere su firma. De hecho, si se usa la herramienta "jar" para añadir ficheros al programa, cambiará el manifiesto, pero no el fichero de firmas. Así que su firma con el certificado no se ve alterada y seguirá siendo válido la firma digital, aunque el ZIP en sí sea completamente diferente. Si se da el caso, para alertar de este hecho, durante la verificación simplemente se mostrará un warning de este tipo:

Warning:
This jar contains unsigned entries which have not been integrity-checked.

Como siempre, una firma válida habla de la integridad (y posible procedencia, según el certificado) de la aplicación, pero nada de sus intenciones sobre el sistema...


Sergio de los Santos
ssantos@11paths.com

No hay comentarios:

Publicar un comentario en la entrada