Firma electrónica XAdES-BES: Soluciones comunes

Luego de más de un año de ser publicada la primera parte de esta serie de artículos sobre la firma electrónica XAdES-BES de los comprobantes tributarios electrónicos del Ecuador, con una segunda parte donde se detalla el proceso de ingeniería inversa que fue utilizado, y tras numerosas implementaciones exitosas en varias empresas, a continuación se presenta un listado de problemas comunes encontrados y la manera en que fueron solucionados.

Soluciones a problemas con firma electrónica XAdES-BES
Photo by looseid

No se puede enviar la información por el servicio web del SRI

Este error por lo general se debe a que no se ha codificado previamente el comprobante electrónico a Base 64. Otra causa suele ser el código de autorización, el cual es un código de 49 dígitos con la siguiente estructura:

  • Ocho (8) dígitos para la fecha de emisión, en formato DDMMYYYY, es decir dos dígitos para el día, dos dígitos para el mes y cuatro dígitos para el año.
  • Dos (2) dígitos para el código del tipo de comprobante:
    • 01 factura,
    • 04 nota de crédito,
    • 05 nota de débito,
    • 06 guía de remisión,
    • 07 retención.
  • Trece (13) dígitos para el RUC, tomando en cuenta las validaciones y los problemas que tienen estos códigos, los cuales ya fueron hablados en otro artículo al respecto del RUC.
  • Un (1) dígito para el tipo de ambiente:
    • 1 es pruebas
    • 2 es producción.
  • Tres (3) dígitos para el establecimiento (por defecto 001).
  • Tres (3) dígitos para el punto de emisión (por defecto 001).
  • Nueve (9) dígitos para el secuencial.
  • Ocho (8) dígitos para el código único.
  • Un dígito para el tipo de emisión, donde 1 es normal.
  • Un dígito para el dígito verificador, el cual se lo calcula con el módulo 11 de los anteriores 48 dígitos.

La implementación de un generador de este código de autorización se lo puede encontrar en el repositorio de GitHub, en la implementación de ejemplo que se hizo para el algoritmo de firmado.

El hash de los nodos y/o la firma electrónica XAdES-BES no coincide con lo generado por el facturador del SRI

Este problema por lo general surge por mal formato de la estructura y/o los caracteres del archivo XML del comprobante:

  • En Windows, la mayoría de editores de texto agregan dos caracteres en cada salto de línea: CR (Carriage Return o retorno de carro, expresado también como \r) y LF (Line Feed o salto de línea, expresado también como \n). Esto puede hacer fallar al cálculo de los hash y de la firma electrónica XAdES-BES, por lo que primero se deberían quitar del texto los caracteres CR.
  • Se debe revisar que la codificación de caracteres sea correcta, por lo general UTF-8.
  • No se admite que el XML tenga tags con auto cierre, es decir en lugar de <tag /> se debería poner <tag></tag>.
  • Hay que revisar con mucho cuidado los espacios antes, y después de los tags, así como los saltos de línea. Por ejemplo en la forma canónica no se deben quitar los saltos de línea, solo hay que agregar los namespaces. También hay que revisar los espacios entre los atributos del tag.
  • Se debe tener estricto respeto de mayúsculas y minúsculas, por ejemplo si el atributo se llama PropertyID, daría error poner PropertyId, con la «d» final minúscula.

La firma electrónica XAdES-BES generada no es correcta

Los archivos .P12 pueden tener dentro varios certificados y varias llaves privadas, las cuales a veces no son leídas por las librerías utilizadas. En algunos casos se deben extraer estos datos, en especial la clave privada, para realizar la firma en sí. Esto se debe a que dentro del archivo P12 pueden haber dos llaves privadas: una para verificación y otra para realizar el firmado. Esto a veces confunde a las implementaciones que leen el archivo P12 y solo cargan la clave privada de verificación. Se producen fallas silenciosas al cargar la clave privada incorrecta para el firmado. De igual manera, se debe tener cuidado en tomar el certificado correcto para el firmado, que pueden ser cuatro.

Dependiendo de la tecnología que se escojan para la implementación, es más o menos fácil implementar la firma electrónica XAdES-BES. Esto se debe a la estructura del certificado digital que genera el Banco Central, el cual contiene dos llaves privadas. Tal situación puede confundir a las librerías de firma electrónica XAdES-BES, ya que toman la primera llave privada cuando deberían tomar la segunda. Por ejemplo, en PHP no hay manera con las librerías estándar de tomar la segunda llave privada, ya que por defecto toma solamente la primera y no da opción de escoger. Lo que se ha tenido que realizar en estos casos es extraer la segunda llave privada en un archivo aparte a través de la herramienta OpenSSL, para luego realizar la firma electrónica XAdES-BES con esta llave privada extraída.

Extracción manual de la llave privada válida

Una vez instalado OpenSSL se pueden extraer las llaves privadas de un certificado del Banco Central con el siguiente comando:

openssl pkcs12 -in juanito_perez.p12 -nocerts -out llaves_privadas_juanito_perez.pem

Al abrir el archivo generado .PEM con cualquier editor de texto se encontrará con dos llaves privadas: la una dice Decryption Key y la otra dice Signing Key. El truco es extraer esta segunda llave privada con el comentario «Signing Key» a un archivo aparte (incluyendo los renglones del principio y final de la llave, con el texto BEGIN ENCRYPTED PRIVATE KEY y END ENCRYPTED PRIVATE KEY), para luego realizar la firma electrónica con este archivo en lugar de usar todo el certificado .P12 y así evitar el uso de la llave privada con comentario Decryption Key en la obtención de la firma electrónica XAdES-BES.

Conclusión

La implementación de la firma electrónica XAdES-BES es un proceso que se lo puede realizar en cualquier lenguaje de programación. Este desarrollo requiere de mucho cuidado para poder realizarlo bien. Se debe evaluar con mucho cuidado si vale la pena realizar esta implementación, ya que en la actualidad ya existen librerías en Java de comprobada efectividad que realizan esta tarea muy bien, y que han sido probadas por varios años en sistemas en producción. Las ventajas de una implementación en otro lenguaje serán:

  • El control de todo el sistema dentro de una misma tecnología;
  • El manejo de casos especiales que no soportan las librerías ya implementadas; y,
  • De paso también el aprendizaje que se llega a adquirir, que no es poca cosa.

Sin embargo, en la gran mayoría de los casos reales, estas ventajas son superadas por las desventajas. Todo sistema nuevo trae un riesgo: errores no detectados que se filtran a producción. Es por ello que se debe evaluar con mucho cuidado la conveniencia de realizar esta implementación. En caso de juzgar conveniente su realización, es recomendable contar con un plan de pruebas para el control de calidad.

41 comentarios sobre “Firma electrónica XAdES-BES: Soluciones comunes

  1. Excelente colaboracion. La encontre de casualidad. Tengo una sola pregunta. Para calcular en PHP etsi:certdigest ds:digestvalue he realizado lo siguiente: genere con openssl un archivo con formado de codificacion .der partiendo de la clave_publica.pem y posteriormente calcule con sha1 y base64_encode. Esta es la forma correcta de calcularlo? Quisiera saber ya que todavia no me funciona la autorizacion del SRI y sigo teniendo el error 39 FIRMA INVALIDA.

    1. Hola. La forma de calcular el ds:digestvalue está bien, pero debes tener cuidado ya que en PHP la función openssl_pkcs12_read no reconoce todos los certificados del archivo .P12 sino solo el primero. Lo que tocaría hacer es extraer el certificado correcto para el firmado con comandos externos, revísale con todo el contenido de la variable que te devuelve openssl_pkcs12_read, en el elemento extracerts del arreglo:

      $certificado_p12 = file_get_contents(«nombre_del_propietario_del_certificado.p12»);
      if (openssl_pkcs12_read($certificado_p12, $pkcs12, «MiClaveDelCertificado»)) {
      $certificado = $pkcs12[«extracerts»][0];
      $certificado = str_replace(‘—–BEGIN CERTIFICATE—–‘, », $certificado);
      $certificado = str_replace(‘—–END CERTIFICATE—–‘, », $certificado);
      $certificado = str_replace(«\n», «», $certificado);
      $certificado = str_split($certificado, 76);
      $certificado = implode(«\n», $certificado);
      $certificado_b64 = str_replace(«\n», «», $certificado);
      $hash_certificado_der = base64_encode(hash(«sha1», base64_decode($certificado_b64), true));
      echo $hash_certificado_der;
      }

    2. Buenas tardes Juan Carrillo
      Tengo Una pregunta Juan Carrillo el etsi:certdigest se calcula con el sha1 de la clave publica suministrada por el proveedor?

    3. Hola amigo tal vez le hizo funcionar la firma electrónica, por que me sale el mismo error. Gracias.

    4. A finales del 2019, el SRI realizó cambios en su definición de la factura electrónica. Se está desarrollando una cuarta parte de esta serie de artículos para finales del 2020, donde se recogerán estos cambios.

  2. hola Edgar Valarezo…
    tengo una pregunta, no he podido obtener solo un nodo para canonicalizar…
    utilizo domDocument de PHP.. me podrias hacer el favor de indicarme..
    gracias

    1. Yo hago a mano la canonicalización, es decir solo agrego los namespaces al XML y nada más.

    1. Está en desarrollo una cuarta parte de este artículo para finales de este año, donde se actualizará el tema con los cambios recientes del SRI y los aportes de la comunidad.

  3. Estuve navegando mucho en la web buscando temas sobre el reto de la facturación electrónica, actualmente estoy desarrollando un modulo de facturacion electronica pero me he quedado en la parte de envìo del xml firmado al sri, no se si en tu próximo blog comentaras esta parte de autorización de comprobantes?
    Saludos y muchos exitos…

  4. Buenos dias, yo estoy siguiendo el uso de su codigo, y sin embargo no logro que la forma devuelva el valor correcto, resento el error 39, pienso que se debe a que el archivo .p12 del banco central tiene dos claves.
    Como puedo acceder a la clave de firma? aun no logro extraerla correctamente.

    Muchas gracias.

    1. Si se te complica demasiado, utiliza una implementación sobre Java, ya que son muy confiables y bastante probadas.

  5. Buen dia, seria seguro ejecutar este código en el lado del cliente ? , el cliente podria ver la contraseña y el certificado ?

    1. No sería buena idea correrlo en el lado del cliente, ya que el frontend es un entorno comprometido. Lo ideal sería correrlo del lado del servidor, o en el lado del cliente pero leyendo directo desde un token PKCS 11, que proteja a la clave privada de ser copiada o transferida.

  6. Buen aporte,sin embargo tengo el mismo error que menciona Juan Carrillo estoy realizando Pruebas desde SoapUI y me rechaza el documento con el error 39
    La firma es invalida [Firma inválida (firma y/o certificados alterados)]
    Espero tus Comentarios

    1. En el artículo 2 se describe la manera de depurar la implementación del algoritmo, y en el artículo 3 se detallan algunos errores comunes.

  7. Hola solo quería agradecerle estimado Ing Valarezo por su pronta respuesta, no había visto esta tercera parte, en php la extracción de la llave privada es correcta, pero coger la llave que tiene Signing Key no, en mi caso vienen 6 private keys, y 6 public keys, lo que hice fue recorrer las public hasta encontrar la que este vigente a la fecha, esa la coloco en el xml y recorro las private keys en busca de la par que contenga ese certificado publico vigente y ya dejo de salir firma no valida, pero me gustaría encontrar alguna forma de sacar las private keys sin usar openssl desde consola, de todas formas muchas gracias por su colaboración, no hay información de esto en Internet, espero mi experiencia les pueda ser útil.

  8. Gracias por el muy buen aporte. Sin embargo encontré la solución para el error de firma inválida en la autorización. El problema está en la función sha1_base64 debería aceptar otro parámetro (codificación) para ser llamada desde la encriptación del comprobante con el argumento «utf8″. Quedando así:
    function sha1_base64(txt, codificacion=») {
    var md = forge.md.sha1.create();
    md.update(txt,codificacion);
    return new window.buffer.Buffer(md.digest().toHex(), ‘hex’).toString(‘base64’);
    }
    y al momento de llamar únicamente desde la encriptación del comprobante
    var sha1_comprobante = sha1_base64(comp, ‘utf8’);
    Además se debe tener en cuenta que las cabeceras del xml cuando se genera con el software del SRI cambian
    /* esta es la correcta para la firma*/

    Además deben borrar todas las líneas en blanco luego de y el salto de línea es con \n no con \n\r
    Si desean probar la firma del xml antes de enviar al ambiente de pruebas pueden hacer uso de este software que es gratuito y muy intuitivo http://www.xolido.com/lang/
    Esto por si a alguna persona le sirve
    Saludos y repito gracias por el aporte

    1. Milton, qué tal, he hecho los cambios en la función sha1_base64 tal como indicaste, pero aún me sale el mensaje de Firma Inválida; qué más se debe hacer?

    2. En el artículo 2 se describe la manera de depurar la implementación del algoritmo, y en el artículo 3 se detallan algunos errores comunes.

    3. Hola Milton, tengo el mismo error, realice todo pero aun tengo ese error, sera acaso las cabeceras xml que mencionas cuales son estas cabeceras porque no se leen en tu comentario.

      saludos

  9. Me sirvió mucho tu artículo, gracias a él pude firmar los xml, después de muchos intentos, tengo algunas soluciones cuando da el error de firma inválida, que quisiera que lo puedas poner en el blog para que a las demás personas les pueda ayudar. Muchas Gracias.

  10. Buenas tardes por casualidad han intentado enviar documentos xml al ws del sri con la función validarComprobante() para su recepción en php.?
    He intentado en de varias formas y ninguna me ha funcionado, esta es una parte de mi script donde realizo dicha acción si alguien me podría ayudar diciéndome en que me equivoco se lo agradecería.

    $xml_base64=base64_encode($ruta_xml_firmado.’1001201901179244632500110010010000000341234567614.xml’);

    $xml =»».
    «».
    «».
    «».
    «».$xml_base64.»».
    «».
    «».
    «»;
    $a= [«xml»=>$xml];
    dd($cliente->validarComprobante($a));

    Cabe destacar que siempre me retorna «DEVUELTO» con el msj «Se encontró el siguiente error en la estructura del comprobante: No se ha encontrado información en el tag claveAcceso.»

  11. Tengo un error que es exactamente este «La firma es invalida [Firma inválida (firma y/o certificados alterados)]», a que se debe, a la firma o al certificado, si es el certificado como podría revisar si es válido? Gracias de antemano.

    1. En el artículo 2 se describe la manera de depurar la implementación del algoritmo, y en el artículo 3 se detallan algunos errores comunes.

  12. Hola estuve probando el código que coloco en jsfiddle y al enviarlo para autorización al SRI me arroja el error de firma invalida, por favor tiene algún consejo para solucionarlo ? Gracias

    1. En el artículo 2 se describe la manera de depurar la implementación del algoritmo, y en el artículo 3 se detallan algunos errores comunes.

  13. Gracias por la Colaboración, Una pregunta Amigo se puede tener el certificado alojado en un hosting, traer el contenido del certificado con ajax y poder utilizar la función de firma que nos indicas en este aporte.
    Si se puede me podrias dar una luz de como hacerlo, gracias.

    1. No es seguro tener viajando al certificado, y lo ideal es que la clave privada de la firma resida en un token PKCS 11, es decir un dispositivo físico especializado para que la clave privada no pueda ser copiada ni transferida. La firma en JavaScript es solo un ejercicio donde se muestra la implementación del algoritmo de la firma electrónica, y usarlo en la vida real parece ser una mala idea. Una posibilidad para salvar este código sería ejecutarlo del lado del servidor, con Node.js.

  14. Saludos
    Si me pudieran ayudar estoy realizando el proceso de facturación electrónica en PHP y gracias a la ayuda presentada en el sitio ya obtengo el certificado válido, el módulo, y el DigestValue del comprobante y realice la prueba firmando con el facturador del sri y los valores son los mismos pero no se si al crear el DigestValue del Certificate o el DigestValue de la Signature estoy haciendo algo mal porque al validar el comprobante me sigue apareciendo el error de Firma inválida.

    De antemano agradezco su respuesta

    Muchas gracias.

    1. Para depurar esos errores puede servir de guía el artículo dos. En resumen, se toma una factura generada y firmada por el facturador del SRI, y luego se iguala tanto la factura como los números aleatorios en el algoritmo implementado, y a través de su implementación deberían de obtenerse los mismos hash que en el facturador del SRI. En caso de haber diferencias, ya tendría pistas de por dónde está la falla.

  15. Las nuevas firmas electrónicas emitidas desde el 27 de julio me arrojan el error:

    Código 39: Firma inválida. No tiene Cadena de Confianza Valida.

    Los documentos anteriores se autorizan sin inconvenientes. Alguna idea?

    1. Sería de probar con el facturador del SRI para ver si también se replica el error, o sino es algo en la obtención de la firma del certificado, donde tal vez se cambió el orden o pasó algo con la estructura interna del archivo P12 emitido.

  16. Hola a todos, espero me puedan ayudar con mi problema :c …. Esto tratando de usar este algoritmo para poder agregar la firma electrónica a la factura en el navegador, pero al momento que uso el programa del SRi para enviar el comprobante (PRUEBAS) la verificación del mismo tiene como resultado NO AUTORIZADO, firma invalida. He comparado el archivo xml firmado que me da el programa del SRI versus el xml que me genera el algoritmo en javascript creado en este foro y me doy cuenta que el valor de X509Certificate es distinto, help me please :c.

  17. Hola Amigos muy buen aporte, si me pueden ayudar con un problema que se genera en el ambiente de pruebas del sri a la hora de autorizar el comprobante, me sale el siguiente error:
    FIRMA INVALIDA: El certificado utilizado, no es del tipo firma digital. Pero en el el ambiente de producción no tengo ningún problema.
    Gracias.

    1. Tal vez sea el dígito que define el ambiente sobre el que se está trabajando, y capaz se lo tiene fijo indicando ambiente de producción, por lo que daría error en el ambiente de pruebas.

  18. Estimados
    Envió el archivo desde el facturador electrónico offline gratuito, firmado desde mi aplicación en php el archivo es igual, me funciona perfectamente, pero al consumir el servicio web desde mi aplicación me devuelve el siguiente error (No se pudo convertir en Certificado X509)
    $datos = ‘

    ‘ . $xml . ‘

    ‘;
    $curl = curl_init();
    curl_setopt_array($curl, array(
    CURLOPT_URL => «https://celcer.sri.gob.ec/comprobantes-electronicos-ws/RecepcionComprobantesOffline?wsdl»,
    CURLOPT_RETURNTRANSFER => 1,
    CURLOPT_SSL_VERIFYPEER => 0,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_POST => 1,
    CURLOPT_HTTPHEADER => array(«Content-Type: text/xml; charset=utf-8″),
    ));
    curl_setopt($curl, CURLOPT_POSTFIELDS, $datos);
    $respuesta = curl_exec($curl);
    $error = curl_error($curl);
    curl_close($curl);
    if ($error) {
    echo ‘Error: ‘ . json_encode($error) . »;
    } else {
    echo ‘Respuesta: ‘ . json_encode($respuesta) . »;
    }
    echo «Completado Envio XML»;

    $data = ‘

    ‘ . $documento . ‘

    ‘;
    $curl = curl_init();
    curl_setopt_array($curl, array(
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_URL => «https://celcer.sri.gob.ec/comprobantes-electronicos-ws/AutorizacionComprobantesOffline?wsdl»,
    CURLOPT_POST => 1,
    CURLOPT_HTTPHEADER => array(«Content-Type: text/xml; charset=utf-8″)
    ));
    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    $respuesta = curl_exec($curl);
    $error = curl_error($curl);
    curl_close($curl);
    if ($error) {
    echo ‘Error: ‘ . json_encode($error) . »;
    } else {
    echo ‘Respuesta: ‘ . json_encode($respuesta) . »;
    }
    echo «Completado Webservice Autorizado Xml»;

    1. Tal vez sean los \r si se lo está implementando en Windows, los cuales se los deben quitar. Otra posibilidad puede ser que falte de agregar en la cabecera la versión del XML y la codificación de caracteres, es decir: «». En cualquier caso, algo debe estar fallando en la estructura.

  19. Buen día
    la firma me funciona correctamente, pero ahora no sé como como cambiar la nueva disposición que la firma lleve el «Digital Signature»

    1. A finales del 2019, el SRI realizó cambios en su definición de la factura electrónica. Se está desarrollando una cuarta parte de esta serie de artículos para finales del 2020, donde se recogerán estos cambios.

  20. Muchas gracias estimado Edgar Valarezo, por el excelente aporte, con la explicacion suya pude implementar la firma electronica, en GeneXus de forma nativa, sin dll o jar.
    Sus publicaciones han sido de mucha ayuda, muchas gracias, es hasta ahora el unico que ha explicado cada uno de los calculos que se realizan para firmar electronicamente.

Deja una respuesta