Crear sello de CFDI con java

El sello de un CFDI es en realidad la firma electrónica de algunos campos en conjunto(cadena original) del CFDI. La firma digital brinda varios servicios de seguridad:

  • No repudio. La entidad que emite el CFDI no puede rechazar que ella/él lo emitió puesto que en teoría sólo ella/él conoce la llave privada y nadie mas.
  • Autenticación. Al verificar la firma de un CFDI sabemos quién lo emitio ya que se utiliza la correspondiente llave pública del emisor. En Criptografía de llave pública,
    siempre existen un par de llaves, la pública y privada, las cuales sirven para cifrar y descifrar el mensaje.
  • Integridad. Al firmar el CFDI se realiza un subproceso que genera una cadena única, producto del uso de funciones hash. Si se altera el documento posteriormente o al menos los
    campos que se uilizan para generar la cadena original, la cadena única generada debe cambiar completamente y el resultado de la firma será diferente a la original.

Hay dos procesos principales en la generación del sello:

  • Obtener una cadena única que identifica a la cadena original y por consecuencia al CFDI. Esto se realiza utilizando un algoritmo que usa funciones hash como SHA256 para CFDI 3.3 (SHA1 para CFDI 3.2)
  • Cifrar con la llave privada la cadena única generada en el paso anterior. Para este paso se utiliza el algoritmo RSA.

En código java se ve de la siguiente manera:

...
//archivoLlavePrivada: es el archivo de la llave privada
//keyWord es el password de la llave privada
PKCS8Key pkcs8 = new PKCS8Key(Base64.getDecoder().decode(archivoLlavePrivada), keyWord.toCharArray());
KeyFactory privateKeyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec pkcs8Encoded = new PKCS8EncodedKeySpec(pkcs8.getDecryptedBytes());
PrivateKey privateKey = privateKeyFactory.generatePrivate(pkcs8Encoded);
signature = Signature.getInstance("SHA256withRSA");
signature.update(cadenaOriginal);
String firma = new String(Base64.getEncoder().encode(signature.sign()));

logger.debug("Firma digital del CFDI:" + firma);
...

El archivo que contiene la llave privada se puede obtener así:

...
File file = new File(path); /path: es la ruta del archivo de llave privada
FileInputStream fileInputStream = new FileInputStream(file);
byte[] fileBytes = new byte[fileInputStream.available()];
fileInputStream.read(fileBytes);
fileInputStream.close();
               
String fileString = new String(Base64.getEncoder().encode(fileBytes));
               
return fileString;
...

Notas:

  • Certificado digital. Este es una estructura de datos que entre otras cosas contiene la llave pública que posteriormente será ocupada para verificar la firma.
  • Realmente el documento oficial es el XML, puesto que contiene los atributos para generar la cadena original, generar el sello y verificar el CFDI. El PDF no importa ya que no es un documento que se utilice para la verificación del sello.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

No me Genera el Sello

Intento generar del sello de esta manera

FileInputStream fin = null;
        try
        {
            fin = new FileInputStream (RutaArchKey);
            PKCS8Key pkcs8 = new PKCS8Key(fin, ContrasenaClavePrivada.toCharArray());
           
            //obtiene clave privada
            java.security.PrivateKey pk = pkcs8.getPrivateKey();
            Signature firma;
           
            firma = Signature.getInstance("SHA256withRSA");
           
            firma.initSign(pk);
           
            //pasamos firma original
            firma.update(cadena.getBytes("UTF-8"));
           
            //cadena encriptada
            byte[] cadenaFirmada = firma.sign();
           
            //sello digital
            String sello_Digital = new String(Base64.encodeBase64(cadenaFirmada));
            comprobante.setSello(sello_Digital);
       
            //return sello_Digital;
        }catch (Exception e){ String sello_Digital = "ERROR SELLO DIGITAL: " + e; //return sello_Digital; }
       
        fin.close();

y no me marca ningún error, pero al mandar a timbrar me dice que el sello no corresponde, que estoy haciendo mal?
ojala me puedas orientar

Verificar cadena original

Primero deberías verificar que la cadena original que tú generas sea igual a la cadena original que el PAC genera, con el XML que tú envías. Normalmente los PACs tienen web services de prueba para timbrar tus XML y generalmente regresan el error que encuentran al no poder timbrar tu CFDI. El error que comunmente ocurre es que se genera mal la cadena original y el PAC genera de manera adecuada la cadena original, así que el sello evidentemente es diferente. Por que ejemplo, tu cadena original puede contener un número con dos decimales y el PAC la genera con un número de 6 decimales.

Mi cadena es correcta

Ya verifique y la cadena original que genero y la que regresa mi pac son identicas

Por ahi vi que en vez de PKCS8 ahora es con PKCS12 pero no se como aplicarlo, ademas creo que debo descargar otra libreria y la forma de aplicarla es diferente, seguire buscando, si alguien de la pagina puede orientarme, estare muy agradecido

Verificar

El formato de la clave privada no ha cambiado, sigue siendo PKCS8, lo puedes verificar en el estandar (http://sat.gob.mx/informacion_fiscal/factura_electronica/Documents/cfdv3..., página 102). Tego un proyecto de CFDI si gustas puedes verificar el test (https://github.com/estepuma/cfdi33-core/blob/master/cfdi33-xml/src/test/...), espero te ayude.

Imagen de jsmaster

Validar XML

Que errores te devuelve el validador de XML? con eso podrías darte una idea más especifica si algún elemento no cumple con el valor esperado.