jueves, 18 de febrero de 2010

Como obtener el sello digital con JAVA

Hola el objetivo de esta entrada es explicar cómo crear un sello digital, para la facturación electrónica.

Escribo esto porque tuve muchos problemas para poder crearlo, mi primer problema fue el poder leer la llave privada que te da el SAT, ya que esta viene ecriptada con un password y encontré poca información de como leerla en java.

Bueno, basta de quejas, empecemos!

Lo primero que hay que hacer es generar la cadena original, la creación de esta cadena esta especificada en el anexo 20 de la Resolución Miscelánea Fiscal inciso D. Esta es una cadena formada con los datos de la factura, estos datos están separados por el carácter pipe ”|”, y se inicia y finaliza esta cadena con un doble pipe “||”.
Aquí le s dejo un ejemplo de la cadena Original.

||2.0|AB|32|2010-02-18T12:30:03|434|2006|ingreso|EFECTIVO|3000.00|45029.85|02934INFOSOFT|Infosoft & Xgress|arenas|98|Coyoacan|DF|México|54832|4503495WXGF4|México|2|Impresoras|1500.00|3000.00|2|PC|2000.00|4000.00|IVA|15.00|2900.85|2900.85||

Ya que tenemos la cadena original lo siguiente es pasarla por un algoritmo de digestión como es el MD5, y el resultado pasarlo por un algoritmo de encriptación RSA, estos dos pasos se pueden hacer en uno solo con el api de java. Para esto lo primero que necesitamos es la clave privada (archivo.key) que nos proporcionó el SAT junto con su password. Esta clave esta codificada según el estándar PKCS8 en formato DER. Además esta clave esta encriptada con un password.
Para poder des enriptarla utilizaremos la librería commons-ssl de apache. De esa libraría utilizaremos el objeto PKCS8Key de la siguiente manera:

PKCS8Key pkcs8 = new PKCS8Key(clavePrivada, password.toCharArray());

Donde clavePrivada es un arreglo de bytes que contiene la clave privada.
Para poder obtener la clave privada solo se la pedimos a este objeto:

java.security.PrivateKey pk = pkcs8.getPrivateKey();

Lo siguiente que hay que hacer es obtener el objeto que codificara en MD5 y RSA nuestra cadena, e inicializarlo con la clave privada este es java.security.Signature.

Signature firma = Signature.getInstance("MD5withRSA");
firma.initSign(pk);

donde pk es la clave privada que obtuvimos.
Al objeto Signature se le pasa la cadena original convertida en bytes con formato UFT-8
firma.update(cadenaOriginal.getBytes("UTF-8"));
y obtenemos el arreglo de bytes con la cadena original encriptada.

byte[] cadenaFirmada = firma.sign();

A continuación se codifica en base 64 la cadena para poder obtener caracteres imprimibles con el objeto sun.misc.BASE64Encoder.

BASE64Encoder b64 = new BASE64Encoder();
String selloDigital = b64.encode(firma.sign());

Y con esto obtenemos una cadena como esta que es el sello digital

UdjknEGo/r0v7QrAhvL+aFKkl6Jk6b4pNvRYI0ymWrW19k4DjzygbtTnAnB0HNmafifTNzxB+/wE
JsJKGWjbnSAO61gy6JTLSvrdS+KPKpQtxYH8/7Ib55J8T4PuIL0a6qmAwpMFrHToSaAO0NXG3W9t
homWp8b+UdbJ2qL/ABI=

Les dejo el código completo para que lo revisen.


package com.infosoft.rasengan.cfd;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.Signature;

import org.apache.commons.ssl.PKCS8Key;
import org.jdom.Document;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jdom.output.Format.TextMode;

import sun.misc.BASE64Encoder;

import com.infosoft.rasengan.cfd.bean.Comprobante;
/**
* Clase que genera el comprobante fiscal digital asi como el sello digital
* @author Xgress
*
*/
public class GeneradorCFD {

private Comprobante comprobante;

/**
* Constructor que inicializa el objeto con un Comprobante, genera el
* selloDigital y se lo agrega al objeto Comprobante
*
* @param comprobante
* Objeto que contiene todos los datos de un Comprobante Fiscal
* Digital
* @param archivoClavePrivada
* InputStream que contiene el archivo .key de la llave privada
* @param password
* String con el password de la llave privada
* @throws GeneralSecurityException
* Excepcion arrogada cuando el password no es el correcto
*/
public GeneradorCFD(Comprobante comprobante,
InputStream archivoClavePrivada, String password)
throws GeneralSecurityException {
this.comprobante = comprobante;
this.comprobante
.setSello(getSelloDigital(archivoClavePrivada, password));
}

/**
* Metodo que genera el sello digital a partir de la cadena original y la
* clave privada siguiendo los siguientes pasos: 1)Se le aplica la funcion
* hash de digestion MD5 a la cadena original y se aplica el algoritmo de
* encripcion RSA con la clave privada. 2) Se codifica el resultado a Base64
* teniendo como resultado una cadena imprimible.
*
* @param archivoClavePrivada
* InputStream que contiene la clave privada(archivo .key)
* @param password
* String con el password de la clave privada
* @return String con el sello digital
* @throws GeneralSecurityException
* Excepcion arrogada cuando el password no es el correcto
*/
public String getSelloDigital(InputStream archivoClavePrivada,
String password) throws GeneralSecurityException {
String cadenaOriginal = comprobante.getCadenaOriginal();
System.out.println(cadenaOriginal);
byte[] clavePrivada = getBytes(archivoClavePrivada);
PKCS8Key pkcs8 = new PKCS8Key(clavePrivada, password.toCharArray());
PrivateKey pk = pkcs8.getPrivateKey();
Signature firma = Signature.getInstance("MD5withRSA");
firma.initSign(pk);
String selloDigital = null;
try {
firma.update(cadenaOriginal.getBytes("UTF-8"));
BASE64Encoder b64 = new BASE64Encoder();
selloDigital = b64.encode(firma.sign());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println(selloDigital);
return selloDigital;
}

/**
* Genera el xml extensible del comprobante fiscal digital
* @return String con el xml
*/
public String getCFD() {
Format format = Format.getPrettyFormat();
format.setEncoding("utf-8");
format.setTextMode(TextMode.NORMALIZE);
XMLOutputter xmlOutputer = new XMLOutputter(format);
String res = xmlOutputer
.outputString(new Document(comprobante.getXML()));
try {
res = new String(res.getBytes("UTF-8"));//TODO: probar bien
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return res;
}
/**
* Metodo que convierte un input stream con la llave privada a un array de bytes
* @param is InputSteam con la clave privada
* @return Arreglo de bytes con la clave privada
*/
private byte[] getBytes(InputStream is) {
int totalBytes = 714;
byte[] buffer = null;
try {
buffer = new byte[totalBytes];
is.read(buffer, 0, totalBytes);
is.close();
} catch (IOException e) {
e.printStackTrace();
}
return buffer;
}

}