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.