Tuesday, April 9, 2013

Generación de Sello de Factura Digital con OpenSSL en C#

Actualización: código disponible en GitHub

Si has llegado aquí, seguramente ya sabes que la facturación digital es un desmadre. Y la peor parte es la generación del sello a partir de la cadena original. De hecho, en general, todo lo relacionado con la criptografía es muy complicado, pero hace tiempo un alma bondadosa creo OpenSSL, una "alternativa de Open Source para implementar SSL" que afortunadamente, para los que están tristemente casados con Windows, también se distribuye como .exe.

El openssl.exe se puede usar con de la linea de comandos. En el caso de las facturas digitales, se puede usar para firmar una cadena original (que es un resumen de toda la información de una factura digital) usando los archivos .cer y .key que componen la FIEL que el SAT entrega a los contribuyentes.


Una cadena original es una simple cadena de texto y tiene la siguiente forma (por ejemplo):

||2.0|ABCD|2|03-05-2010T14:11:36|49|2008|INGRESO|UNA SOLA EXHIBICIÓN|2000.00|00.00|2320.00|PAMC660606ER9|CONTRIBUYENTE PRUEBASEIS PATERNOSEIS MATERNOSEIS|PRUEBA SEIS|6|6|PUEBLA CENTRO|PUEBLA|PUEBLA|PUEBLA||MÉXICO|72000|CAUR390312S87|ROSA MARÍA CÁLDERON URIEGAS|TOPOCHICO|52|JARDINES DEL VALLE|NUEVO LEÓN|MEXICO|95465|1.00|SERVICIO|01|ASESORIA FISCAL Y ADMINISTRATIVA|2000.00|IVA|16.00|320.00||

Para obtener un sello a partir de esa cadena original, lo primero que se debe hacer es guardarla en un archivo de texto con codificación UTF-8 sin BOM. No intenten hacerlo con el Bloc de Notas, no sirve de nada. Se necesita un editor de texto como Notepad++ que permite elegir la codificación del archivo.


Una vez que se tiene el archivo cadena.txt (o como le hayan llamado), el segundo paso es crear un archivo .pem a partir del archivo .key de la FIEL porque será necesario para el próximo paso. Para hacer esto, hay que abrir la línea de comandos y usar el siguiente comando:

openssl pkcs8 -inform DER -in c:/ruta/a/miarchivo.key -passin pass:contraseña -out c:/ruta/a/miarchivo.pem

Nota: yo recomiendo que en cuanto terminen de usar el .pem lo borren porque el archivo no esta protegido por la contraseña y si alguien lo obtiene podría firmar fácilmente documentos a nombre del dueño de la FIEL.

Ahora que ya se tiene el archivo .pem, se puede firmar la información (la cadena original en cadena.txt) usando el siguiente comando:

openssl dgst -sha1 -sign c:/ruta/a/miarchivo.pem c:/ruta/a/cadena.txt > c:/ruta/a/sellobinario.txt

El archivo resultante del paso anterior (sellobinario.txt) ¡es el sello!, pero, aún no puede usarse porque está escrito en bytes, y si lo abrimos con un editor de texto veremos caracteres raros que seguramente no son parte del estándar UTF-8 con el que se tienen que representar los XMLs. Por eso el SAT exige que ese sello se reescriba en formato Base64. OpenSSL puede ayudar con el comando:

openssl base64 -in c:/ruta/a/sellobinario.txt -out c:/ruta/a/sello.txt

Ahora sí, el archivo sello.txt contiene el sello que podemos agregar en el XML de la factura digital. Sin embargo, este procedimiento se puede automatizar. En mi caso, hice una función de C# (pero debería poderse lograr algo parecido en otros lenguajes del mismo nivel como Java o PHP):

public string Sellar(string keyFile, string pass, string cadena)
{
    string path = "C:\\algun\\directorio";
   
    // Escribir archivo UTF8 de la cadena
    var tempCadena = path + "\\openssl\\cadena" + DateTime.Now.ToString("yyMMddhhmmss");
    System.IO.File.WriteAllText(tempCadena, cadena);

    // Digestion SHA1
    var tempSha = path + "\\openssl\\sha" + DateTime.Now.ToString("yyMMddhhmmss");
    var opensslPath = path + "\\openssl\\openssl.exe";
    Process process = new Process();
    process.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
    process.StartInfo.FileName = opensslPath;
    process.StartInfo.Arguments = "dgst -sha1 " + tempCadena;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.ErrorDialog = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.Start();

    String codificado = "";
    codificado = process.StandardOutput.ReadToEnd();
    process.WaitForExit();

    String codificado2 = "";
    // Si quieren cambiar este ciclo por un string.IndexOf('='), adelante, yo soy muy flojo.
    for (int i = 0; i < codificado.Length; i++)
    {
        if (codificado[i] == '=')
        {
            codificado2 = codificado.Substring(i + 2);
            break;
        }
    }
    System.IO.File.WriteAllText(tempSha, codificado2);

    // Crear .pem del .key
    var tempPem = path + "\\openssl\\pem" + DateTime.Now.ToString("yyMMddhhmmss");
    Process process2 = new Process();
    process2.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
    process2.StartInfo.FileName = opensslPath;
    process2.StartInfo.Arguments = "pkcs8 -inform DER -in " + keyFile + " -passin pass:" + pass + " -out " + tempPem;
    process2.StartInfo.UseShellExecute = false;
    process2.StartInfo.ErrorDialog = false;
    process2.StartInfo.RedirectStandardOutput = true;
    process2.Start();
    process2.WaitForExit();

    // Generar sello
    Process process3 = new Process();
    process3.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
    process3.StartInfo.FileName = opensslPath;
    process3.StartInfo.Arguments = "dgst -sha1 -sign " + tempPem + " " + tempCadena;
    process3.StartInfo.UseShellExecute = false;
    process3.StartInfo.ErrorDialog = false;
    process3.StartInfo.RedirectStandardOutput = true;
    process3.Start();
   
    // Codificar en Base64
    String selloTxt = process3.StandardOutput.ReadToEnd();
    String b64 = Convert.ToBase64String(Encoding.Default.GetBytes(selloTxt));
    process3.WaitForExit();
   
    // Por aquí deberían borrar los archivos temporales, ¿ya les dije que soy flojo?
   
    return b64;


Espero que les haya sido de ayuda.

22 comments:

  1. Excelente esto si que sirve muchas gracias por hacer esto

    ReplyDelete
  2. Muy clara tu explicación, Muchas gracias :)

    ReplyDelete
  3. Sabes me la pase 4 dias leyendo foros y blogs y nada, pero por que nadie se habia tomado la molestia de decir un dato que mencionas en tu post tan insignificante pero que cambia todo. Eres grande!!!!!!

    ReplyDelete
  4. esto es para php es donde se notan las ventajas de este jejeje

    $priv_key_id=openssl_get_privatekey("file://$filekey.pem");
    $o=openssl_sign($cadenaoriginal,$cadenafirmada, $priv_key_id,OPENSSL_ALGO_SHA1);
    $sello=base64_encode($cadenafirmada);

    ReplyDelete
  5. paso el código en C# que me funciono, el sat valida el sello y se usa para timbrar nominas.


    //FUNCION PARA GENERAR CADENA ORIGINAL APARTIR DE UN XML
    private string GenerarCadena(String archivo ) {
    try{
    StreamReader reader= new StreamReader( archivo);//archivo es la ruta del archivo xml

    XPathDocument myXPathDoc = new XPathDocument(reader);
    XslCompiledTransform myXslTrans = new XslCompiledTransform();
    String CadOrigXSLT = Application.StartupPath + @"\cadenaoriginal_3_2.xslt";//tambien tienes q tener el archive nomina1.1
    myXslTrans.Load(CadOrigXSLT);
    StringWriter str = new StringWriter();
    XmlTextWriter myWriter = new XmlTextWriter(str);

    //'Aplicando transformacion
    myXslTrans.Transform(myXPathDoc,null, myWriter);

    //'Resultado

    String result = str.ToString();
    result = result.Replace("\n", "");//quitamos los saltos de linea


    reader.Dispose();
    reader.Close();
    return result;//result trae la cadena original
    }
    catch (InvalidCastException e)
    {
    MessageBox.Show(e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    return "";
    }
    }


    ReplyDelete
  6. //CREAMOS EL SELLO

    private string CrearSello(String rutaArchivo )
    {
    try
    {
    //'obtenemos la cadena para crear el archivo cadena.txt
    String cadenaOriginal = GenerarCadena(rutaArchivo);
    String rutaDir = Application.StartupPath;
    String codificado;
    String codificado2;
    String rutaKey = rutaDir + @"\rutakey.key";
    String passCer = "password
    String SelloTxt=""; //inicializamos
    String sello="";
    //'A continuacion guardaremos la cadena en un archivo con su formato UTF-8 como lo pide SAT
    File.WriteAllText(rutaDir + @"\cadena.txt", cadenaOriginal);

    //PIRMERO CREAMOS EL .PEM
    // //'CREAMOS EL ARCHIVO .KEY.PEM
    Process process2 = new Process();// process2.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
    process2.StartInfo.FileName = rutaDir + @"\OpenSSL\bin\openssl.exe";//'le ponemos el nombre del archivo q hara la encriptacion (la ruta del openssl.exe)
    String argumento2 = "pkcs8 -inform DER -in " + rutaKey + " -passin pass:" + passCer + " -out " + rutaDir + @"\aaa010101aaa.key.pem";
    process2.StartInfo.Arguments = argumento2;// 'le madamos solo un parametro.
    process2.StartInfo.WorkingDirectory = rutaDir + @"\OpenSSL\bin";//'esto es para poner el direcctorio inicial en el q iniciara el proceso

    process2.StartInfo.UseShellExecute = false;//'esto es para decirle q no usaremos el shell del sistema operativo
    //process2.StartInfo.ErrorDialog = false ;//'//esto es para omitir cualquier mensaje de error del proceso a ejecutar
    process2.StartInfo.RedirectStandardOutput = true; //'esto es para decirle q vamos a escribir (o utiliar) el resultado de la secuencia (o sea el resultado encriptado)
    process2.Start();//'iniciamos el proceso
    process2.WaitForExit();//'le decimos q se espere el proceso

    //OBTENER EL SELLO
    Process process5 = new Process();//obtenemos el sello, se hacen 2 pasos juntos
    process5.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
    process5.StartInfo.FileName = rutaDir + @"\OpenSSL\bin\openssl.exe";//'le ponemos el nombre del archivo q hara la encriptacion (la ruta del openssl.exe)
    String argumento5 = "dgst -sha1 -sign " + rutaDir + @"\aaa010101aaa.key.pem " + rutaDir + @"\cadena.txt";// process5.StartInfo.Arguments = argumento5;//'le madamos solo un parametro.
    process5.StartInfo.WorkingDirectory = rutaDir + @"\OpenSSL\bin";//'esto es para poner el direcctorio inicial en el q iniciara el proceso
    process5.StartInfo.UseShellExecute = false;// 'esto es para decirle q no usaremos el shell del sistema operativo
    //process3.StartInfo.ErrorDialog = false ;//'esto es para omitir cualquier mensaje de error del proceso a ejecutar
    process5.StartInfo.RedirectStandardOutput = true;// 'esto es para decirle q vamos a escribir (o utiliar) el resultado de la secuencia (o sea el resultado encriptado)
    process5.Start();//'iniciamos el proceso
    SelloTxt = process5.StandardOutput.ReadToEnd();
    String b65 = "";
    b65=Convert.ToBase64String(Encoding.Default.GetBytes(SelloTxt));//se decodifica en base 64


    return b65;// sello;
    }

    catch (InvalidCastException e)
    {
    MessageBox.Show(e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    return "";
    }
    }




    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. Está muy bien la explicación, solamente aclarar que cuando mencionas "usando los archivos .cer y .key que componen la FIEL que el SAT entrega a los contribuyentes." no se refiere a la FIEL sino al CSD generado a partir de la FIEL con la aplicación solcedi del SAT, es decir un contribuyente puede generar 1 o más CSD's y se pueden utilizar no solamente para sellar cadenas originales de comprobantes fiscales, también se genera uno distinto para sellar archivos de control volumétrico de gasolineras por ejemplo... Gracias por la info me fue muy util y saludos....

    ReplyDelete
  9. https://eplauchu.wordpress.com/2016/02/21/signing-an-string-with-sha1-rsa-and-bash/ <--- i ported your solution to linux red hat 7

    ReplyDelete
  10. Por si a alguien todavía les sirve, retomé el código de un usuario de aquí (Jackzon) y le agregue algo que hacía falta para sellar el XML CFDI:

    //FUNCION PARA GENERAR EL SELLO DEL XML CFDI
    private string CrearSello(String rutaArchivo)
    {
    try
    {
    //Se declaran las variables
    String rutaDir = @"D:\Rutaorigen\";
    String rutaKey = rutaDir + "aaa010101aaa__csd_02.key";
    String passCer = "12345678a";
    String SelloTxt = "";
    String cadenaOriginal = GenerarCadena(rutaArchivo);
    File.WriteAllText(rutaDir + @"\cadena.txt", cadenaOriginal);//Se obtiene cadena

    //SE CREA EL .PEM DEL KEY
    Process process1 = new Process();
    process1.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
    process1.StartInfo.FileName = @"C:\OpenSSL\openssl.exe";//la ruta del openssl.exe
    String argumento1 = "pkcs8 -inform DET -in " + rutaKey + " -passin pass:" + passCer + " -out " + rutaDir + @"\aaa010101aaa.key.pem";
    process1.StartInfo.Arguments = argumento1;//se envía la instrucción
    process1.StartInfo.WorkingDirectory = @"C:\OpenSSL";
    process1.StartInfo.UseShellExecute = false;
    process1.StartInfo.RedirectStandardOutput = true;
    process1.Start();//'iniciamos el proceso
    process1.WaitForExit();

    //OBTENER EL SELLO
    Process process2 = new Process();
    process2.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
    process2.StartInfo.FileName = @"C:\OpenSSL\openssl.exe";//la ruta del openssl.exe
    String argumento2 = "dgst -sha1 -out " + rutaDir + @"\sign.bin -sign " + rutaDir + @"\aaa010101aaa.key.pem " + rutaDir + @"\cadena.txt";
    process2.StartInfo.Arguments = argumento2;//se envía la instrucción.
    process2.StartInfo.WorkingDirectory = @"C:\OpenSSL";
    process2.StartInfo.UseShellExecute = false;
    process2.StartInfo.RedirectStandardOutput = true;//
    process2.Start();//'iniciamos el proceso
    process2.WaitForExit();

    //OBTENER EL SELLO B64
    Process process3 = new Process();
    process3.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
    process3.StartInfo.FileName = @"C:\OpenSSL\openssl.exe";//la ruta del openssl.exe
    String argumento3 = "enc -in " + rutaDir + @"\sign.bin -a -A ";//se envía la instrucción
    process3.StartInfo.Arguments = argumento3;
    process3.StartInfo.WorkingDirectory = @"C:\OpenSSL";
    process3.StartInfo.UseShellExecute = false;
    process3.StartInfo.RedirectStandardOutput = true;
    process3.Start();//'iniciamos el proceso
    SelloTxt = process3.StandardOutput.ReadToEnd();//Se obtiene el sello en Base64

    return SelloTxt;// sello;
    }
    catch (Exception ex)
    {
    return "Error al sellar: " + ex.ToString();
    }
    }
    Saludos.
    Atentamente: IsmaKeps

    ReplyDelete
    Replies
    1. Y para generar la cadena utilizar:

      //FUNCION PARA GENERAR CADENA ORIGINAL APARTIR DE UN XML
      private string GenerarCadena(String archivo)
      {
      try
      {
      StreamReader reader = new StreamReader(archivo);//archivo es la ruta del archivo xml

      XPathDocument myXPathDoc = new XPathDocument(reader);
      XslCompiledTransform myXslTrans = new XslCompiledTransform();
      String CadOrigXSLT = @"D:\Rutaorigen\cadenaoriginal_3_2.xslt";
      myXslTrans.Load(CadOrigXSLT);
      StringWriter str = new StringWriter();
      XmlTextWriter myWriter = new XmlTextWriter(str);

      //'Aplicando transformacion
      myXslTrans.Transform(myXPathDoc, null, myWriter);

      //'Resultado
      String result = str.ToString();
      result = result.Replace("\n", "");//quitamos los saltos de linea

      reader.Dispose();
      reader.Close();
      return result;//result trae la cadena original
      }
      catch (Exception ex)
      {
      return "Error al generar la cadena: " + ex.ToString();
      }
      }

      Delete
  11. Excelentes aportaciones. Gracias por su tiempo y sobre todo por compartir!!

    Aprovechando la oportunidad ¿Alguien tiene un ejemplo de cómo hacer la conexión por SOAP y carga de certificado para autenticarse con un WS para timbrado?

    ReplyDelete
  12. https://github.com/pianodaemon/cfdiengine/tree/master/crypto

    ReplyDelete
  13. en el momento de firmar la informacion me sale un error dgst -sha1 -sign C:\ruta\miarchivo.pem C:\ruta\cadena.txt > C:\ruta\sellobinario.txt
    que dice no such file or directory error in dgst, alguien sabe porque?

    ReplyDelete