Dónde va la firma dentro del XML de un comprobante: los dos bloques que SUNAT valida por separado y sus veintiséis rechazos
El primer comprobante que firmas con una librería nueva casi nunca se rechaza por la criptografía. Se rechaza porque el bloque de firma está en el sitio equivocado, o porque falta el otro bloque, el que nadie recuerda que existe. Y el mensaje de SUNAT es literal hasta el punto de resultar inútil: te devuelve la ruta del tag que falta y nada más.
Las reglas de validación de comprobantes de pago electrónicos que SUNAT publica en su portal, en la versión actualizada al 26 de agosto de 2026, traen una hoja llamada «Firma». Son veintiséis validaciones repartidas en dos bloques del documento que no tienen nada que ver entre sí. Vale la pena leerlas antes de escribir la primera línea, porque explican qué comprueba SUNAT y, sobre todo, qué no comprueba.
Bloque uno: la firma de verdad, dentro de ext:UBLExtensions
La firma XMLDSig va colgada de esta ruta:
ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature
UBL 2.1 no define un hueco propio para la firma electrónica en el cuerpo de la factura. Define un contenedor de extensiones, y SUNAT lo usa para meter ahí la estructura ds:Signature del estándar XML Signature. De ahí que la ruta sea tan larga y que a la firma haya que llegar atravesando tres niveles antes de tocar nada criptográfico.
SUNAT valida nueve elementos dentro de ese bloque, y a cada uno le asigna dos códigos de rechazo: uno para cuando el tag no existe y otro para cuando existe con un formato que no cumple.
| Elemento | Falta el tag | Formato incorrecto |
|---|---|---|
ds:Signature/@Id | 2085 | 2084 |
ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm | 2087 | 2086 |
ds:SignedInfo/ds:SignatureMethod/@Algorithm | 2089 | 2088 |
ds:SignedInfo/ds:Reference/@URI | 2091 | 2090 |
ds:Reference/ds:Transform/@Algorithm | 2093 | 2092 |
ds:Reference/ds:DigestMethod/@Algorithm | 2095 | 2094 |
ds:Reference/ds:DigestValue | 2097 | 2096 |
ds:SignatureValue | 2099 | 2098 |
ds:KeyInfo/ds:X509Data/ds:X509Certificate | 2101 | 2100 |
El patrón se ve de un vistazo y ahorra tiempo de depuración: el código impar significa que el tag no está y el par que está mal escrito. La pareja 2076 y 2077, del segundo bloque, es la única que rompe la regla, así que conviene no fiarse de ella.
El rechazo 2090, que es el que se lleva más horas
De los dieciocho códigos de la tabla, diecisiete describen un tag ausente o un formato que no cuadra. Uno dice otra cosa. El 2090 valida ds:SignedInfo/ds:Reference/@URI con esta condición: el atributo tiene que estar vacío. El mensaje de retorno lo dice sin adornos, «Debe estar vacio para id».
Un URI vacío en XML Signature significa firmar el documento entero, incluida la propia envoltura, y no una parte referenciada por identificador. Es lo que corresponde a una firma envolvente, que es el modelo que usa el comprobante peruano. Muchas librerías de firma XML rellenan ese atributo por defecto con el identificador del nodo firmado, porque en la mayoría de los usos de XMLDSig eso es lo correcto, y esa opción por defecto es la que produce un rechazo que no se entiende leyendo el mensaje.
El ds:Transform que acompaña a esa referencia tiene la misma lógica: con la firma envolvente hay que excluir el propio nodo de firma del cálculo del resumen, o el documento cambia al firmarlo y la verificación nunca cuadra. SUNAT valida que el atributo @Algorithm exista y tenga formato, pero no comprueba en esta hoja qué algoritmo pusiste; eso se decide más adelante, cuando se verifica la firma.
Si estás decidiendo qué formato usar para documentos que no son comprobantes, la comparación entre PAdES y XAdES y las decisiones que no se pueden deshacer están en este artículo.
Bloque dos: cac:Signature, que no firma nada
Aquí es donde tropiezan casi todas las integraciones nuevas. Además del bloque criptográfico, el comprobante tiene que llevar un segundo bloque, colgado directamente de la raíz del documento:
/Invoice/cac:Signature
Ese bloque no contiene firma. Contiene los datos UBL del firmante: un identificador, el RUC y el nombre de quien firma, y una referencia externa al bloque de firma real. Es información descriptiva, y SUNAT la exige con la misma dureza que la otra:
cac:Signature/cbc:ID— 2076 si falta, 2077 si está vacío. Esta pareja va al revés que todas las demás.cac:SignatoryParty/cac:PartyIdentification/cbc:ID— 2079 si falta, 2078 si el contenido no cuadra.cac:SignatoryParty/cac:PartyName/cbc:Name— 2081 si falta, 2080 si el formato no cumple.cac:DigitalSignatureAttachment/cac:ExternalReference/cbc:URI— 2083 si falta, 2082 si el formato no cumple.
El interesante es el 2078. Su condición, tal como la escribe la hoja de validaciones, es que el campo «debe ser igual al RUC del emisor o al RUC que se envía el comprobante». El mensaje de retorno, en cambio, se queda corto y solo dice «Debe ser igual al RUC del emisor». La condición manda sobre el mensaje, y lo que dice la condición es que SUNAT admite ahí el RUC de un tercero que envía por ti. Quién puede tener el certificado con el que se firma y qué dice el reglamento de firmas sobre eso es otra conversación, y está en este artículo de hoy mismo.
El cbc:URI del último elemento es el que ata los dos bloques: apunta al @Id del ds:Signature que va dentro de las extensiones. Si el identificador de un lado y la referencia del otro no coinciden, tienes dos bloques válidos por separado y un documento que no se sostiene.
Lo que estos veintiséis códigos no comprueban
Ninguno. Ni uno solo de los veintiséis verifica la firma.
Las validaciones de formato de esta hoja son de una simplicidad que sorprende: casi todas comprueban que el contenido sea «alfanumérico de hasta 3000 caracteres», y las de ds:SignatureValue y ds:X509Certificate piden letras de la A a la Z, números, los símbolos + y =, y un mínimo de dos caracteres. Es decir, comprueban que parezca base64. Un comprobante con una firma copiada de otro documento pasa las veintiséis.
La comprobación criptográfica ocurre después y devuelve otros códigos. El 2335 dice «El documento electrónico ingresado ha sido alterado», que es lo que sale cuando el resumen no cuadra, y el 2336, «Ocurrió un error en el proceso de validación de la firma digital». Los rechazos que miran el certificado en sí, del 2325 al 2328, están explicados uno a uno aquí.
Y hay un código que no es rechazo y que conviene conocer porque se comporta distinto: el 1059, «El XML no contiene firma digital». Los códigos por debajo de 2000 son excepciones, no errores de validación, así que SUNAT no genera constancia de recepción: el servicio responde con una excepción del programa y el comprobante se considera no informado. La diferencia importa para el código que escribas alrededor, porque no hay un CDR que archivar.
El orden de depuración que ahorra tiempo
Con esa separación clara, la secuencia que funciona es siempre la misma:
- Valida contra el XSD antes de enviar. El manual del programador de SUNAT lo pide expresamente y lo llama parseo. La mitad de los códigos por debajo de 2000 se cazan ahí, en local, sin gastar un envío.
- Comprueba que existen los dos bloques. Si solo tienes
ds:Signature, la primera respuesta de SUNAT va a ser un 2079 o un 2083 y vas a buscar el problema en la criptografía, que está bien. - Verifica que el
@Idy elcbc:URIcoinciden. - Mira el
@URIde la referencia. Si tu librería lo rellenó, vacíalo. - Solo entonces preocúpate por la firma. Si llegas a un 2335 es buena noticia: la estructura ya está bien y lo que falla es el cálculo o la canonicalización.
Ese último punto tiene un matiz que cuesta caro descubrir. Si el documento se modifica después de firmarlo, aunque sea para arreglar un espacio en blanco o reindentar el XML, el resumen deja de cuadrar y el rechazo es un 2335. Firmar tiene que ser lo último que le pasa al documento antes de comprimirlo y enviarlo.
Cómo comprobar la firma sin depender de SUNAT
El validador oficial del Estado acepta comprobantes en XML y dice qué reconoce y qué no. Qué comprueba de verdad y qué no comprueba está en este artículo, y para un entorno de desarrollo sigue siendo la comprobación más rápida antes de enviar nada. La guía completa de integración, con las decisiones de arquitectura que vienen antes de todo esto, está en el artículo pilar.
En mifirmadigital.pe emitimos los certificados con los que se firman estos comprobantes y hemos depurado unas cuantas integraciones contra estos mismos códigos. Si tienes un rechazo que no entiendes, escríbenos con el código y el fragmento del XML donde va la firma.