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;
}

}

49 comentarios:

j. isaac legorreta dijo...

Hola sabes como obtener los datos pero con los pagos vía applet, del ejercicio la cadena original, es que no tengo idea como hacerlo, y obtengo la cadena original pero no trae los conceptos para generar la llave de pago, por cierto buena aportación

Anónimo dijo...

Como se importa el archivo .key?

Alex Sánchez dijo...

Mi querido amigo, muchas gracias por compartir este conocimiento. "Ya llevaba varias greñas arrancadas" y nadamás no daba cómo hacer esto. Y sí, coincindo contigo, mucha datos pero poca información.

Signus dijo...

Hola Amigo, excelente aporte, solo me queda una duda, y perdonaras la ofensa... donde encuentro el commons-ssl? solo he encontrado not-yet-commons-ssl. Saludos y gracias!

Anónimo dijo...

Gracias... me ahorraste bastante tiempo...

http://gulsin.org

Anónimo dijo...

Excelente... muchas gracias por el aporte

Anónimo dijo...

Muchas gracias por tu aporte, sin embargo podrías pasarnos elobjeto factura porfavor...

Xgress dijo...

Hola, primero que nada una disculpa por no responder, soy nuevo en esto de los blogs y no me habia dado cuenta que tenia comentarios,

voy a tratar de contestar a sus preguntas. Primero que nada el jar de commons-ssl lo pueden bajar de aqui: http://www.java2s.com/Code/Jar/ABC/Downloadcommonsssljar.htm

Con respecto a como importar el archivo .key pues primero lo lees con FileInputStream de esta manera:

FileInputStream fis = new FileInputStream("c:/aaa010101aaa_CSD_10.key");

Despues lo tienes que pasar a un byte[]

en mi caso hize un metodo que me lo convertia:

public static byte[] getBytes(InputStream is) {
int totalBytes = 1024;
byte[] buffer = null;
try {
buffer = new byte[totalBytes];
is.read(buffer, 0, totalBytes);
} catch (IOException e) {
e.printStackTrace();
}
return buffer;
}

y despues lo importante es pasarlo a un objeto PrivateKey, para esto primero lo tuve que poner en un objeto PKCS8Key de la siguiente manera:

PKCS8Key pkcs8 = new PKCS8Key(bArray, "password".toCharArray());

y despues obtuve la Privatekey

PrivateKey pk = pkcs8.getPrivateKey();

Xgress dijo...

Por cierto a que te refieres con el objeto factura?

bBTo25 dijo...

muy bien Ing. por compartir sus grandes conocimientos. Cuidese y siga adelante

Julio dijo...

Gracias por la información.

Juan Jose Enriquez dijo...

Excelente aporte, he estado buscando información sobre como leer el certificado y la llave, aunque me falta buscar como generar el XML, si alguien sabe como PUBLIQUE, otra cosa perdon por la PREGUNTA pero ya has probado que la información que obtienes sea la correcta, no se con algun certificado de prueba o que hayas hecho alguna validación en el portal del SAT

Anónimo dijo...

Hola alguien sabe como integrar este codgo en visual basic 6, o el algorito del sello digital, si alguien lo tiene ayudarme por favor
rojas.arturo@prodigy.net.mx

Anónimo dijo...

hola Xgreess

Muchas gracias , Muy buen aporte, si no es mucha molestia se agradeceria que nos pasaras la clase comprobante

Anónimo dijo...

Muy buen proceso, tambien me ha dado mucha problema eso de leer el certificado pero con esto avanzare un buen :D.

jorjonio dijo...

Hola quye tal
Tengo una duda
package com.infosoft.rasengan.cfd;

esa clase onde la consigo

jejejeje

gracias y excelente aporte

Anónimo dijo...

-----------------------------------
Jorge Armando dijo...
Hola quye tal
Tengo una duda
package com.infosoft.rasengan.cfd;

esa clase onde la consigo
-----------------------------------
Jorge
"package com.infosoft.rasengan.cfd;"
Es el nombre del paquete donde esta la clase o archivo java donde esta desarrollado el codigo, no es una clase sino el paquete, si vas a copiar y pegar el codigo en un nuevo archivo java omite copiar esa linea.

Unknown dijo...

hola gracias por tu excelente post. podrias de favor y seria de mucha ayuda incluir la clase comprobante...Muchas Gracias

Anónimo dijo...

hola !!!

podrias ayudarme a como crear la cadena original soy nuevo en esto y se que datos requiero para crearla pero no se como hacerla estoy tranajando en java te lo agradeceria mucho espero y puedas ayudarme.

Anónimo dijo...

hola!!

un favor me podrias decir como genero la cadena original se que datos requiere pero no se como crearla te lo agradeceria mucho soy nuevo en esto y pues sinceramente no se como hacerlo estoy tabajando en java espero tu respuesta.

Carmen dijo...

Hola como estas, muy buen aporte, gracias..
Tengo una duda, se podria aplicar a otro tipo de documentos, como por ejemplo archivos pdf, u otros? cual seria el procedimiento para generar la cadena orginial?

Agradeceria mucho tu ayuda..

Anónimo dijo...

La instruccion...

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



me esta arrojando este error;
java.lang.NoClassDefFoundError: com/sun/net/ssl/internal/ssl/Provider

El .jar common ssl lo baje de donde lo inidicaste
a ver si me pueden apoyar

saludos
juan arturo gomez
jasistemas@hotmail.com

Anónimo dijo...

Buen día, gracias por el código.
Al aplicarlo obtengo un sello digital que me marca como inválido. Tengo el sello correcto para compararlo, y los veo diferentes.
Hay alguna manera de que en Java se genere igual que como se hace con OpenSSL? Generándola con esa librería no hay problemas y pasa la validación del SAT.
wolfiepop@yahoo.com
Gracias de antemano

Anónimo dijo...

Hola Amigo excelente aporte. Tengo unas dudas nada msa como crear la cadena original de cada uno de los renglones de la factura, me podrias pasara el layout???? o de donde lo puedo bajar... He hecho cosas para VB 2008. las puedo compartir...
jcubedo@yahoo.com

Daniel Aquino dijo...

COmparte el codigo VB2008, gracias

Anónimo dijo...

Hola gracias por el ejemplo, lo he usado pero ha salido una nueva resulucion del Anexo 20 en el cual se especifica que para el calculo del sello hay que emplenar SHA-1 y RSA.

El unico cambio a realizar en el codigo de esta página para que este actualizado es:

Signature firma = Signature.getInstance("SHA1withRSA");

Saludos

Anónimo dijo...

Gracias.

Ya me esta doliendo la cabeza, solo me hacia falta el "UTF-8" para que me funcionara.

Abajo el Abajo el esquema del PAC...!

Unknown dijo...

Gracias por el aporte es lo que buscaba... alguien tiene el codigo del comprobante es lo unico que falta... gracias.

tmsanchez dijo...

Puedes generar las clases de java para el comprobante a partir del XSD del SAT solo utiliza el comando XJC que viene con el JDK 1.6.

Simplemente haz lo siguiente desde una línea de comandos:

1) Copia el archivo XSD del sat a una carpeta y dentro de ella crea un folder que se llame src

2) Ejecuta la siguiente instrucción desde una línea de comandos:

xjc cfdv2.xsd -p mx.gob.sat.cfds.beans -d src\

PD. en Windows el xjc.exe está en la carpeta BIN del JDK

Espero les sea de utilidad.

ISC.ALEJANDRO dijo...

Hola
Esta muy bien tu blog
Tengo una duda para saber si el sello digital es valido vez que en el validador de hacienda te dice el sello presenta problemas
pero la verdad ya busque pero no encuentro nada

MaRkO dijo...

Hola, muy buena la recomendacion, mira yo estoy tratando de hacer una solucion para esto, pero quiero leer la informacion del certificado como lo puedo hacer o con que clases lo puedo lograr, lo q me interesa es obtener la vigencia. me podrias ayudar, te lo agradeceria =)

ISC.ALEJANDRO dijo...

La verdad para leer el contenido del .cer no he encontrado nada en java
pero yo estoy checando los archios desde el sat son .txt trae la vigencia de folios etc.
te dejo el link para que lo cheques,
ftp://ftp2.sat.gob.mx/agti_ftp/cfds_ftp/
mi duda seria como saber si el sello esta bien
asi como el validador
para implantar el receptor ya nadamas me falta checar el sello
Espero me puedas ayudar!!

Anónimo dijo...

Hola, oye muchisimas gracias por esta aportación, llevaba algunos dias leyendo como leer el archivo .key para poder ocuparlo y hacer el sign y no econtraba nada, excelente trabajo.

ISC.ALEJANDRO dijo...

Tengo un problema para verificar el sello digital sea valido ,quiero saber si es valido con md5 y con sha1
pero como ??????

Anónimo dijo...

Oye, en la siguiente pagina del sat: https://www.consulta.sat.gob.mx/SICOFI_WEB/ModuloECFD_Plus/ValidadorComprobantes/Validador.html, entiendo puedo validar un documento xml creado, todo parece estar correcto excepto el sello, me sale un mensaje que indica "El sello del comprobante no es válido", tienes idea de por que podria ser?, Gracias

Anónimo dijo...

Ese error es por que tienes mal en encriptado con md5 o puede que te falta alguna variable de las que lleva la cadena original

malagon dijo...

Excelente info, me ahorro bastante tiempo, por cierto, tengo un problema, el típico funciona excelente en mi maquina de desarrollo con java:
java version "1.6.0_12"
Java(TM) SE Runtime Environment (build 1.6.0_12-b04)
Java HotSpot(TM) Client VM (build 11.2-b01, mixed mode, sharing)
Pero en producción con java:
java version "1.4.2_07"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2_07-b05)
Java HotSpot(TM) Client VM (build 1.4.2_07-b05, mixed mode)
Obtengo el siguiente error:
Server.userException : java.lang.reflect.InvocationTargetException

Cualquier comentario es agradecido.

Anónimo dijo...

A alguien aqui, el sello se lo ha validado correctamente, ya cheque mi cadena original contra la que el sat me genera en su validador deacuerdo al xml que lee y es exactamente la misma, pero por algun motivo mi sello no le gusta, alguna idea??

public String Sello(){
String result=null;
try {
Signature rsa;
rsa = Signature.getInstance("Sha1withRSA");
try {
FileInputStream fis = new FileInputStream("C:\\cer\\archivo.key");
byte[] clavePrivada = getBytes(fis);
try {
PKCS8Key pkcs8 = new PKCS8Key(clavePrivada, "clave".toCharArray());
PrivateKey pk = pkcs8.getPrivateKey();
rsa.initSign(pk);
try{
rsa.update(cadenaorig().getBytes("UTF-8"));
BASE64Encoder b64 = new BASE64Encoder();
result = (b64.encode(rsa.sign()));
System.out.println(result);
}catch(Exception ex2){
System.out.println(ex2);
}
} catch (GeneralSecurityException ex) {
Logger.getLogger(FacturaVistaPrevia.class.getName()).log(Level.SEVERE, null, ex);
}
} catch (FileNotFoundException ex) {
Logger.getLogger(FacturaVistaPrevia.class.getName()).log(Level.SEVERE, null, ex);
}

} catch (NoSuchAlgorithmException ex) {
Logger.getLogger(FacturaVistaPrevia.class.getName()).log(Level.SEVERE, null, ex);
}
return result;
}

Eduardo dijo...

Hola, como leo la vigencia del cerificado,

Pau stuff dijo...

hola, pues que cadena original usas? yo estoy usando esta: ||A|1|2005-09-02T16:30:00|1|ISP900909Q88|Industrias del Sur Poniente, S.A. de C.V.|Alvaro Obregón|37|3|Col. Roma Norte|México|Cuauhtémoc|Distrito Federal|México|06700|Pino Suarez|23|Centro|Monterrey|Monterrey|Nuevo Léon|México|95460|CAUR390312S87|Rosa María Calderón Uriegas|Topochico|52|Jardines del Valle|Monterrey|Monterrey|Nuevo León|México|95465|10|Caja|Vasos decorados|20|200|1|pieza|Charola metálica|150|150|IVA|52.5||
y tengo un algoritmo para generar el md5, y lo que me genera el md5 es 8aa2b617944427353697e694a2e35a07. y esto esta correcto, comparandolo con el archivo de prueba del sat.

pero al hacer el sello lo comparo con el que deberia de generar y es totalmente distinto, sabes a que se debe esto?, tu que cadena usas?

Anónimo dijo...

Hola el sello que obtengo con java es diferente a uno generado con openssl, ya que se genera una clave privada.pem
como puedo generar la llave privada.pem con java, esto debería generar el sello correcto, pero no he encontrado como generarlo.

Gracias por el aporte
Saludos

Anónimo dijo...

un lazo!!!!
no logro agregar xsi:schemalocation="http..." al atributo raiz en el xml
uso jdom

se agradece algun tip

tmsanchez dijo...

échale un ojo a esto http://code.google.com/p/factura-electronica/

Unknown dijo...

Alguien sabe si se debe de hacer otro cambio para 2011 aparte de MD5withRSA por SHA1withRSA?
Anteriormente me funcionaba perfecto pero ahora dice sello INVALIDO.

Gracias

Anónimo dijo...

hora hay que hacer temas para el CFDI

Unknown dijo...

para generar en el xml las rutas de los esquemas XSD del sat dejo este codigo que genera el archivo xmlde cfd v3

import java.io.FileWriter;
import java.io.IOException;
import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

public class factura {
static String factura;

public static void main(String[] args) {

}

public static void set_factura(){
try {

Element CFDI = new Element("Comprobante","cfdi","http://www.sat.gob.mx/cfd/3");
Document document = new Document(CFDI);
CFDI.addNamespaceDeclaration(Namespace.getNamespace("xsi","http://www.w3.org/2001/XMLSchema-instance"));
//xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 cfdv3.xsd"
Namespace XSI = Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
CFDI.addNamespaceDeclaration(XSI);
CFDI.setAttribute("schemaLocation", "http://www.sat.gob.mx/cfd/3 cfdv3.xsd",XSI);
CFDI.setAttribute("version","3.2");
//Modificar fecha y hora a la del sitema acual.
CFDI.setAttribute("fecha","2014-01-12T17:05:34");

XMLOutputter xmlOutput = new XMLOutputter();

xmlOutput.output(document, System.out);

xmlOutput.setFormat(Format.getPrettyFormat());
xmlOutput.output(document, new FileWriter("generatedXml.xml"));
//return factura;

} catch (IOException io) {
System.out.println(io.getMessage());
}

}

}

Unknown dijo...

Duda...!!!
me pudieran ayudar a encontar el .jar para
com.infosoft.rasengan.cfd.bean.Comprobante;

Unknown dijo...

Hola!

Tengo una duda, tu aplicación funciona con una applet o con que?

Actualmente estoy desarrollando una aplicación similar con un applet, pero estoy teniendo muchos problemas con las librerías externas al jar del applet por la seguridad de java.

Agradecería algunos tips, gracias.

Unknown dijo...

alguien que tenga todo el proyecto completo??