Páginas

domingo, 27 de mayo de 2012

Manejo del DNIe con javax.smartcardio en Ubuntu

Una pequeña aplicación de ejemplo de acceso al DNIe para firmar y verificar la firma electrónica con el API javax.smartcardio de Oracle.

Son dos clases:
  • AppTest. Clase con el main que desencadena el ejemplo
  • UtilsDnie. Clase con los métodos para detectar el lector, pedir el dni, firmar y verificar la firma
Antes de comenzar aclarar que en el código se incluyen a "fuego" las ubicaciones de un par de librería, por lo que conviene ajustar estos path antes de probar.
  • Librería /usr/lib/i386-linux-gnu/libpcsclite.so de manejo de lectores de tarjetas.
  • Librería /usr/lib/opensc-pkcs11.so de manejo de las funcionalidades criptográficas del DNIe.
Clase AppTest
 
package ord.edu.app;

public class AppTest {

    private static final String TEXTO_A_FIRMAR  = "texto de ejemplo a firmar";
    private static final String PIN_ACCESO_DNIE = "xxxxxxxxxx";

    public static void main(String[] args) throws Exception {

        final UtilsDNIe ud = new UtilsDNIe();

        //Solicitamos el DNIe
        if(ud.solicitaDNI()) {

            //Si se ha insertado el DNIe firmamos
            final byte[] firma = ud.firmaDatos(PIN_ACCESO_DNIE,
                    TEXTO_A_FIRMAR.getBytes());

            //Verificamos la firma
            final boolean resultadoFirma = ud.verificaFirmaDatos(PIN_ACCESO_DNIE,
                    TEXTO_A_FIRMAR.getBytes(),
                    firma);
            System.out.println(resultadoFirma);
        }
    }
}


Clase UtilsDnie
 
package ord.edu.app;

import java.io.ByteArrayInputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.Signature;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.List;

import javax.smartcardio.Card;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.TerminalFactory;

import sun.security.pkcs11.SunPKCS11;

public class UtilsDNIe {

    private static final String CERT_AUTEN = "CertAutenticacion";
    private static final String CERT_FIRMA = "CertFirmaDigital";
    private static final long TIMEOUT_INSERCION_DNIE = 5000;

    /**
     * Solicita al usuario la inclusión del DNIe en cualquiera de los lectores
     * de tarjetas PC/SC detectados.
     *
     * PC/SC (Personal Computer/Smart Card) es un conjunto de especificaciones para la
     * integración de tarjetas inteligentes en ordenadores personales. En particular se
     * define un API de programación que permite a los desarrolladores trabajar de forma
     * uniforme con lectores de tarjetas de distintos fabricantes (que cumplan con la
     * especificación).
     *
     * Como tenemos instalada la libería libpcsclite.so, accedemos al DNIe mediante el
     * proveedor SunPCSC, ajustando la propiedad "sun.security.smartcardio.library" del
     * sistema tal y como se documenta en la URL:
     * http://docs.oracle.com/javase/6/docs/technotes/guides/security/
     * SunProviders.html#SunPCSCProvider
     *
     * @return Card conectado al DNIe
     * @throws CardException Se se ha detectado algú problema con la tarjeta
     */
    public boolean solicitaDNI() throws CardException  {

        System.setProperty("sun.security.smartcardio.library",
                "/usr/lib/i386-linux-gnu/libpcsclite.so");

        //Recorremos la lista de lectores detectado pidiendo el DNIe
        Card card = null;
        CardTerminal terminal;
        final TerminalFactory factory = TerminalFactory.getDefault();
        final List<CardTerminal> terminals = factory.terminals().list();

        for (int i = 0; i < terminals.size(); i++) {

            terminal = terminals.get(i);
            //Por darle un punto interactivo indicamos al usuario
            //que inserte el DNIe y esperamos un poco
            System.out.println("Inserte su DNIe en: "+terminal.getName());
            terminal.waitForCardPresent(TIMEOUT_INSERCION_DNIE);

            if(terminal.isCardPresent()) {
                System.out.println("Detectada tarjeta, conectando");
                //Conectamos usando cualquier protocolo "*"
                card = terminal.connect("*");
                break;
            } else {
                System.out.println("No se ha detectado el DNIe en: "+terminal.getName());
            }           
        }

        //Se ha detectado el DNIe, devolvemos true y desconectamos reseteando la conexión
        if (card!=null){
            card.disconnect(true);
            return true;
        }

        //No se ha detectado el DNIe, devolvemos false
        return false;
    }

    /**
     * Firmamos los datos recibidos en el DNIe
     *
     * @param pinAcceso Número PIN de acceso al DNIe
     * @param datos Datos a firmar
     * @return Datos firmados
     * @throws Exception Problemas durante el proceso de firmado
     */
    public byte[] firmaDatos(String pinAcceso, byte[] datos) throws Exception {

        //Ajustamos el proveedor PKCS11
        final SunPKCS11 sunpkcs11 = ajustaPKCS11ProviderUbuntu();

        //Accedemos al almacen de certificados del DNIe con el pin.
        final KeyStore keyStore = KeyStore.getInstance("PKCS11", sunpkcs11);
        char[] pin = pinAcceso.toCharArray();
        keyStore.load(null, pin);

        //Recogemos la lista de alias de certificados del dispositivo
        final Enumeration<String> enumeration = keyStore.aliases();

        //Buscamos el alias del certificado de Firma, en el DNIe hay dos, uno para
        //firmar (CertFirmaDigital) y otro para autenticarse (CertAutenticacion)
        String alias = null;
        while(enumeration.hasMoreElements()) {

            alias = enumeration.nextElement().toString();
            if(CERT_FIRMA.equals(alias)) {
                break;
            }
        }

        //Se se ha encontrado el certificado de firma seguimos
        if(CERT_FIRMA.equals(alias)) {

            //Recogemos la clave privada del certificado de firma
            final Key key = keyStore.getKey(CERT_FIRMA, pin);

            //Firmamos (en el dnie) los datos recibidos
            final Signature firmador = Signature.getInstance("SHA1withRSA");
            firmador.initSign((PrivateKey)key);
            firmador.update(datos);
            final byte[] firma = firmador.sign();

            //devolvemos la firma
            return firma;

        } else {
            throw new Exception("No se encontro el certificado CertFirmaDigital de firma");
        }
    }

    /**
     * Validamos la firma recibida en el DNIe
     *
     * @param pinAcceso Número PIN de acceso al DNIe
     * @param datos Datos que se han firmado
     * @param firma Firma a verificar
     * @return Resultado de la validación de la firma
     * @throws Exception Problemas durante el proceso de validación
     */
    public boolean verificaFirmaDatos(String pinAcceso, byte[] datos, byte[] firma)
        throws Exception {

        //Ajustamos el proveedor PKCS11
        final SunPKCS11 sunpkcs11 = ajustaPKCS11ProviderUbuntu();

        //Accedemos al almacen de certificados del DNIe con el pin.
        final KeyStore keyStore = KeyStore.getInstance("PKCS11", sunpkcs11);
        char[] pin = pinAcceso.toCharArray();
        keyStore.load(null, pin);

        //Recogemos la lista de alias de certificados del dispositivo
        final Enumeration<String> enumeration = keyStore.aliases();

        //Buscamos el alias del certificado de Firma, en el DNIe hay dos, uno para
        //firmar (CertFirmaDigital) y otro para autenticarse (CertAutenticacion)
        String alias = null;
        while(enumeration.hasMoreElements()) {

            alias = enumeration.nextElement().toString();
            if(CERT_FIRMA.equals(alias)) {
                break;
            }
        }

        //Se se ha encontrado el certificado de firma seguimos
        if(CERT_FIRMA.equals(alias)) {

            //Recogemos el certificado de firma
            final Certificate certificado = keyStore.getCertificate(alias);

            //Validamos la firma de los datos (en el dnie)
            final Signature verificadorFirma = Signature.getInstance("SHA1withRSA");
            verificadorFirma.initVerify(certificado.getPublicKey());
            verificadorFirma.update(datos);

            return verificadorFirma.verify(firma);

        } else {
            throw new Exception("No se encontro el certificado CertFirmaDigital de firma");
        }
    }

    /**
     * Ajustamos el proveedor SunPKCS11 apuntando a las librerías nativas pkcs11
     * del sistema. El proveedor SunPKCS11 no realiza funciones criptográficas
     * por si mismo, sino que permite a las aplicaciones Java usar los APIS JCA/JCE
     * para acceder a estas funcionalidades en las librerías nativas pkcs11.   
     */
    private SunPKCS11 ajustaPKCS11ProviderUbuntu() {

        final String pkcs11config = "name = DNIe\nlibrary = /usr/lib/opensc-pkcs11.so\n";
        final SunPKCS11 sunpkcs11 = new SunPKCS11(
                new ByteArrayInputStream(pkcs11config.getBytes()));
        Security.addProvider(sunpkcs11);
        return sunpkcs11;
        //TODO: Amplicar a otros sistemas operativos
    }
}

5 comentarios:

  1. Un buen recordatorio para hacerlo.

    ResponderEliminar
  2. Amigo, una pregunta. Sabes como matar la conexión con la lectora. Sucede que tengo un proyecto y el System.exit(0) no funciona y es porque como que la conexión entre lectora y java queda aún.

    ResponderEliminar
  3. Hola

    Tuve problemas parecidos pero no era la conexión Java-Lectora sino el navegador que dejaba abierta una sesión con el lector de tarjetas cuando accedía a alguna página https. ¿Es una aplicación de escritorio o un applet?

    Un saludo

    ResponderEliminar
  4. No me funciona, intento copiar el codigo en una raspberry pi y me sale este errir>
    AppTest.java:10: error: cannot find symbol
    final UtilsDNIe ud = new UtilsDNIe();
    ^
    symbol: class UtilsDNIe
    location: class AppTest
    AppTest.java:10: error: cannot find symbol
    final UtilsDNIe ud = new UtilsDNIe();
    ^
    symbol: class UtilsDNIe
    location: class AppTest
    2 errors

    Alguna ayuda

    ResponderEliminar
  5. Hola

    Me da la sensación de que el pseudojava que maneja tu 'raspberry pi' al igual que el pseudojava que maneja Arduino, no entiende la especificación 'final' que meto en el ejemplo para indicar a Java como manejar esa variable en memoria. Elimina los final del código y prueba.

    ResponderEliminar