Para interactuar con una tarjeta inteligente o un dispositivo token con fines de autenticación o firma, el PKCS#11 es uno de los estándares para hacerlo. Java proporciona el proveedor SunPKCS11
, que es una especie de contenedor que podemos usar para interactuar con bibliotecas nativas que manejan la comunicación con este tipo de dispositivos. Básicamente instalamos una librería (archivos dll
, so
, o dylib
) en el sistema operativo, que suele ser proporcionada por el proveedor del dispositivo, y a través del proveedor SunPKCS11
le indicamos el archivo de librería, el proveedor PKCS11 se encarga de interactuar con el dispositivo para nosotros.
Por lo tanto, Java tiene un soporte listo para usar para interactuar con esos dispositivos. Sin embargo, en Java 9 se incluyeron algunos cambios sobre cómo podemos inicializar el SunPKCS11
Provider y esos cambios ya no son compatibles con las versiones anteriores de Java.
SunPKCS11 en Java 8 y anteriores
Los siguientes son los pasos para crear y configurar la instancia SunPKCS11 en Java 8 y versiones anteriores.
import sun.security.pkcs11.SunPKCS11; String configName = "/opt/bar/cfg/pkcs11.cfg"; Provider p = new SunPKCS11(configName); Security.addProvider(p);
Básicamente, dado un archivo /opt/bar/cfg/pkcs11.cfg
con las configuraciones de ese proveedor (archivo de biblioteca, nombre, etc). Pudimos inicializar y configurar el SunPKCS11
en una sola línea, usando el constructor. Finalmente, el nuevo proveedor se agrega al contexto de seguridad.
SunPKCS11 en Java 9 y nuevas versiones
Los siguientes son los pasos para crear y configurar la instancia de SunPKCS11 en Java 9 o versiones posteriores.
import sun.security.pkcs11.SunPKCS11 String configName = "/opt/bar/cfg/pkcs11.cfg"; Provider p = Security.getProvider("SunPKCS11"); p = p.configure(configName); Security.addProvider(p);
En este caso, la clase SunPKCS11
no tiene un constructor disponible para instanciar, y la configuración se realiza en un paso aparte, a través del método configure(String configFile)
. Finalmente, la nueva instancia del proveedor (después de la configuración) se agrega al contexto de seguridad.
Es posible obtener una instancia de SunPKCS11
a través del contexto de seguridad, utilizando el método getProvider
. Devuelve una instancia “vacía” que funciona como una “plantilla”, luego usamos el método de configure
del método.
La razón por la que SunPKCS11
no tiene un constructor público es por la encapsulación del módulo agregada en Java 9. Debido a que pertenece al paquete sun.*
, están restringidos de manera predeterminada para acceder por código.
Una solución para todas las versiones
Como podemos ver, ambos enfoques han cambiado la forma de inicializarlo y configurarlo. Ambos enfoques no son compatibles porque la definición (constructor y métodos) de ambos es diferente.
La solución es utilizar el API de Java Reflection.
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.Provider; import java.security.Security; private static final String SUN_PKCS11_CLASSNAME = "sun.security.pkcs11.SunPKCS11"; private static final String SUN_PKCS11_PROVIDER_NAME = "SunPKCS11"; try { int javaVersion = JavaVersionUtil.getVersion(); if(javaVersion >= 9){ Provider prototype = Security.getProvider(SUN_PKCS11_PROVIDER_NAME); Class<?> sunPkcs11ProviderClass = Class.forName(SUN_PKCS11_CLASSNAME); Method configureMethod = sunPkcs11ProviderClass.getMethod("configure", String.class); return (Provider) configureMethod.invoke(prototype, configFile); } else { Class<?> sunPkcs11ProviderClass = Class.forName(SUN_PKCS11_CLASSNAME); Constructor<?> constructor = sunPkcs11ProviderClass.getConstructor(String.class); return (Provider) constructor.newInstance(configFile); } } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException ex) { //Handle any error, log message or throw an exception }
Básicamente se trata de detectar en tiempo de ejecución la versión de Java que se está ejecutando, y dependiendo de su valor accederá a los diferentes métodos de la clase SunPKCS11
en cada versión. El constructor lo configura en Java 8 y versiones anteriores, o el método configure
en Java 9 o versiones posteriores.
Encontrar la versión de Java en ejecución
No hay ciencia espacial en esta parte, solo verificar el valor en la propiedad del sistema java.version
.
public class JavaVersionUtil { public static int getVersion() { String version = System.getProperty("java.version"); if(version.startsWith("1.")) { version = version.substring(2, 3); } else { int dot = version.indexOf("."); if(dot != -1) { version = version.substring(0, dot); } } return Integer.parseInt(version); } }
Habilitando los módulos crypto y sus paquetes
Desde Java 9 a 15, aún se puede acceder a esos paquetes, pero después de Java 16 no son públicos de forma predeterminada, por lo tanto, debemos agregar algunas cláusulas de módulo para acceder. Eso podría hacerse agregando las siguientes líneas al archivo Manifest.MF
de nuestro jar.
Add-Exports jdk.crypto.cryptoki/sun.security.pkcs11 jdk.crypto.cryptoki/sun.security.pkcs11.wrapper java.base/sun.security.action java.base/sun.security.rsa Add-Opens java.base/java.security java.base/sun.security.util
Eso lo he hecho usando el maven-jar-plugin
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> ..... </manifest> <manifestEntries> .... <Add-Exports>jdk.crypto.cryptoki/sun.security.pkcs11 jdk.crypto.cryptoki/sun.security.pkcs11.wrapper java.base/sun.security.action java.base/sun.security.rsa</Add-Exports> <Add-Opens>java.base/java.security java.base/sun.security.util</Add-Opens> </manifestEntries> </archive> </configuration> </plugin>
Conclusión
Con suerte, este código ayudará a otras personas a lidiar con la misma situación en la que necesitan admitir diferentes versiones de Java (por ejemplo, aplicaciones de escritorio de Java) y quieren proporcionar una solución de versión cruzada.
Referencias
- https://docs.oracle.com/javase/7/docs/technotes/guides/security/p11guide.html
- https://docs.oracle.com/en/java/javase/11/security/pkcs11-reference-guide1.html
- https://en.wikipedia.org/wiki/PKCS_11
- https://www.geeksforgeeks.org/reflection-in-java/