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):
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:
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:
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):
Espero que les haya sido de ayuda.
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
-outc:/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.
Excelente esto si que sirve muchas gracias por hacer esto
ReplyDeleteDe nada :D
DeleteExcelente Gracias.
ReplyDeletePor nada.
DeleteMuy clara tu explicación, Muchas gracias :)
ReplyDeleteDe nada!
DeleteSabes 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!!!!!!
ReplyDeleteJajaja, ¿cuál dato?
Deleteesto es para php es donde se notan las ventajas de este jejeje
ReplyDelete$priv_key_id=openssl_get_privatekey("file://$filekey.pem");
$o=openssl_sign($cadenaoriginal,$cadenafirmada, $priv_key_id,OPENSSL_ALGO_SHA1);
$sello=base64_encode($cadenafirmada);
Buena onda :D
Deletepaso el código en C# que me funciono, el sat valida el sello y se usa para timbrar nominas.
ReplyDelete//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 "";
}
}
//CREAMOS EL SELLO
ReplyDeleteprivate 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 "";
}
}
Gracias :)
DeleteThis comment has been removed by the author.
ReplyDeleteEstá 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....
ReplyDeleteGracias por el comentario
Deletehttps://eplauchu.wordpress.com/2016/02/21/signing-an-string-with-sha1-rsa-and-bash/ <--- i ported your solution to linux red hat 7
ReplyDeletePor 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:
ReplyDelete//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
Y para generar la cadena utilizar:
Delete//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();
}
}
Excelentes aportaciones. Gracias por su tiempo y sobre todo por compartir!!
ReplyDeleteAprovechando 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?
https://github.com/pianodaemon/cfdiengine/tree/master/crypto
ReplyDeleteen 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
ReplyDeleteque dice no such file or directory error in dgst, alguien sabe porque?
Para comprar una casa hace falta una factura digital?
ReplyDeleteEsa es una pregunta para un contador.
DeleteSoy nuevo en este tema, ojala me puedan ayudar. En OpenSSL ejecuto este comando y fuciona muy bien "openssl smime -encrypt -binary -aes-256-cbc -in Archivo.txt -out eArchivo.txt -outform DER certificdo.cer" Mi pregunta es, como puedo ejecutar este comando en C#. Gracias por su apoyo
ReplyDeleteEn el código se dan varios ejemplos de correr un comando de consola. Quizas te ayude más leer la documentación https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.start?view=net-5.0
DeleteSaludos.