Proveedor de PKCS 11 en varias versiones de Java

Advertisements

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.

Advertisements

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

Advertisements

Leave a Reply

Your email address will not be published.