SIFEN_OPERATIVE_LOGIC.md
Lógica Operativa Verificada con DNIT – Base para migración Laravel 12 / PHP 8.5
Fuente: Análisis de
GeneradorXml.php,FirmadorParaguay.phpy archivos enapi-py/. Propósito: Documentar algoritmos y decisiones técnicas que producen documentos aceptados por SIFEN, sin reproducir código legado, para una reimplementación limpia y orientada a objetos.
1. Algoritmo del CDC y Dígito Verificador
1.1 Construcción de la cadena base del CDC (43 dígitos exactos)
El CDC es una cadena de 44 caracteres: 43 de datos + 1 dígito verificador (DV). Se forma concatenando los siguientes campos en este orden y con estos anchos fijos:
| Posición | Campo | Ancho | Relleno | Ejemplo |
|---|---|---|---|---|
| 1–2 | Tipo de Documento (iTiDE) | 2 | 0 a la izquierda | 01 |
| 3–10 | RUC del emisor (sin DV) | 8 | 0 a la izquierda | 03782211 |
| 11 | DV del RUC emisor | 1 | ninguno | 0 |
| 12–14 | Código de establecimiento | 3 | ninguno (ya viene con 3 dígitos) | 001 |
| 15–17 | Punto de expedición | 3 | ninguno | 001 |
| 18–24 | Número del documento | 7 | ninguno (ya viene con 7 dígitos) | 0000001 |
| 25 | Tipo de contribuyente | 1 | ninguno | 1 |
| 26–33 | Fecha de emisión (YYYYMMDD) | 8 | se extrae del ISO datetime quitando guiones y la parte de hora | 20241029 |
| 34 | Tipo de emisión | 1 | ninguno | 1 |
| 35–43 | Código de seguridad aleatorio | 9 | 0 a la izquierda | 000123456 |
Reglas de transformación:
- Tipo de Documento: Si es de 1 dígito, se antepone un
0. - RUC sin DV: Se separa el RUC en la parte numérica y el DV usando
-como delimitador. La parte numérica se rellena con0s a la izquierda hasta completar 8 caracteres. - Fecha: Del campo ISO
YYYY-MM-DDTHH:mm:ss, se toma solo la parte antes de laT, se eliminan los guiones, resultando enYYYYMMDD. - Código de seguridad: Se rellena con
0s a la izquierda hasta completar 9 caracteres.
Nota implementación: El código de seguridad (
codigoSeguridadAleatorio) en producción debe ser un número aleatorio. En el proyecto legado se usósubstr(time(), -9)como aproximación de prueba.
1.2 Algoritmo Módulo 11 para el DV del CDC
Este es el algoritmo canónico que usa la DNIT. Se aplica sobre la cadena de 43 dígitos:
Paso 1 – Normalización alfanumérica Si cualquier carácter del CDC no es un dígito (0-9), se reemplaza por su valor ASCII decimal. En la práctica, el CDC solo contiene dígitos, pero la normalización protege contra edge cases con letras en el RUC o código de seguridad.
Paso 2 – Suma ponderada de derecha a izquierda
k = 2
total = 0
Para cada dígito, iterando de derecha a izquierda:
total += dígito × k
k += 1
Si k > 11:
k = 2 ← el factor se cicla entre 2 y 11
Paso 3 – Cálculo del DV
resto = total % 11
Si resto > 1:
DV = 11 - resto
Si resto <= 1:
DV = 0
Distinción importante: Existe una segunda variante del Módulo 11 en el proyecto (usada para validar RUCs), que itera de izquierda a derecha con factor iniciando en 7 y decreciendo hasta 2, y donde el resultado
11mapea a0y10a1. Esta variante NO se usa para el CDC; solo para validación de RUC del emisor/receptor.
2. Parámetros de Firma Digital (XMLDSig)
2.1 Constantes y URLs de Algoritmos (xmlseclibs)
| Parámetro | Valor / URL | Descripción |
|---|---|---|
| Canonicalization | http://www.w3.org/2001/10/xml-exc-c14n# | Exclusive C14N (EXC_C14N) |
| SignatureMethod | http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 | RSA-SHA256 |
| DigestMethod | http://www.w3.org/2001/04/xmlenc#sha256 | SHA-256 |
| Transform 1 | http://www.w3.org/2000/09/xmldsig#enveloped-signature | Firma enveloped (se auto-excluye) |
| Transform 2 | http://www.w3.org/2001/10/xml-exc-c14n# | Canonicalization del nodo referenciado |
2.2 Nodo de referencia y punto de inserción
La firma es de tipo Enveloped Signature (la firma va dentro del documento que firma):
Para Documentos Electrónicos (DE - facturas, notas de crédito, etc.):
- Nodo firmado (Reference URI): El elemento
<DE Id="{{cdc}}">— es decir, la referencia apunta al elemento cuyo atributoIdcoincide con el CDC. - Punto de inserción: La firma
<Signature>se inserta como hijo directo del elemento raíz<rDE>.
Para Eventos (Cancelaciones, Inutilizaciones):
- Nodo firmado (Reference URI): El elemento
<rEve Id="1">. - Punto de inserción: La firma
<Signature>se inserta como hijo del elemento<rGesEve>.
2.3 Prefijo de namespace en la firma
La instancia de XMLSecurityDSig se crea con prefijo vacío ('').
Esto produce elementos sin prefijo: <Signature>, <SignedInfo>, <SignatureValue>, etc.
Si se instanciara con 'ds', generaría <ds:Signature> — lo que rechaza la DNIT.
2.4 Certificado X.509 embebido en la firma
El certificado se agrega con los siguientes parámetros:
- Formato PEM:
true(el contenido del P12 se lee y se convierte internamente) - Cadena completa:
false(solo el certificado del firmante, no la cadena CA) - IssuerSerial: Configurable (en producción:
true; en testing puede serfalse)
2.5 Manipulación del XML antes y después de firmar
Antes de firmar:
- El XML es cargado en un
DOMDocumentvíaloadXML(). - Se establece
$doc->encoding = 'utf-8'. - No se realiza ninguna limpieza manual de espacios ni namespaces.
- La canonicalization (EXC_C14N) se encarga de normalizar el XML automáticamente.
Después de firmar:
- Se agrega el bloque QR (
<gCamFuFD><dCarQR>...</dCarQR></gCamFuFD>) como hijo del<rDE>raíz. Esto ocurre fuera del nodo firmado, por lo que no invalida la firma. - Se elimina la declaración XML via
str_replace: se quitan<?xml version="1.0" encoding="utf-8"?>y<?xml version="1.0"?>del resultado desaveXML().
CRÍTICO: La eliminación de la declaración XML es obligatoria porque el XML firmado viaja embebido dentro de un SOAP Envelope que ya tiene su propia declaración.
2.6 QR Code – Construcción del hash
El QR se construye después de firmar (no afecta la firma):
Parámetros del QR (como query string):
nVersion=150
Id={cdc}
dFeEmiDE={fecha en hexadecimal bin2hex}
dRucRec={ruc del receptor} ← O dNumIDRec si no es contribuyente
dTotGralOpe={total general}
dTotIVA={total IVA}
cItems={cantidad de items}
DigestValue={DigestValue de la firma, en bin2hex}
IdCSC={id del código de seguridad del contribuyente}
Hash SHA-256:
secreto = queryString + csc (CSC = Código de Seguridad del Contribuyente)
cHashQR = hash('sha256', secreto)
URL final:
https://ekuatia.set.gov.py/consultas/qr?{queryString}&cHashQR={hash}
Nota: El proyecto legado tiene hardcodeada la URL de test (
consultas-test). En producción usarconsultassin el-test.
3. Mapa de Endpoints y Sobres SOAP
3.1 Tabla de Endpoints
| Operación | Modo | URL |
|---|---|---|
| Envío Síncrono (DE) | TEST | https://sifen-test.set.gov.py/de/ws/sync/recibe.wsdl |
| Envío Síncrono (DE) | PROD | https://sifen.set.gov.py/de/ws/sync/recibe.wsdl |
| Envío Asíncrono (Lote) | TEST | https://sifen-test.set.gov.py/de/ws/async/recibe-lote.wsdl |
| Envío Asíncrono (Lote) | PROD | https://sifen.set.gov.py/de/ws/async/recibe-lote.wsdl |
| Consulta de Lote | TEST | https://sifen-test.set.gov.py/de/ws/consultas/consulta-lote.wsdl |
| Consulta de Lote | PROD | https://sifen.set.gov.py/de/ws/consultas/consulta-lote.wsdl |
| Eventos (Cancel./Inut.) | TEST | https://sifen-test.set.gov.py/de/ws/eventos/evento.wsdl |
| Eventos (Cancel./Inut.) | PROD | https://sifen.set.gov.py/de/ws/eventos/evento.wsdl |
3.2 Estructura de los SOAP Envelopes
Protocolo: SOAP 1.2 (namespace http://www.w3.org/2003/05/soap-envelope)
HTTP: POST, Content-Type: application/xml; charset=utf-8
Declaración XML: <?xml version="1.0" encoding="UTF-8" standalone="no"?>
Envío Síncrono (un DE):
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Header/>
<env:Body>
<rEnviDe xmlns="http://ekuatia.set.gov.py/sifen/xsd">
<dId>1</dId>
<xDE>{XML_FIRMADO_SIN_DECLARACION}</xDE>
</rEnviDe>
</env:Body>
</env:Envelope>
Respuesta → env:Body > hijo con prefijo ns2 → rRetEnviDe
Envío Asíncrono (lote ZIP en Base64):
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Header/>
<env:Body>
<rEnvioLote xmlns="http://ekuatia.set.gov.py/sifen/xsd">
<dId>1</dId>
<xDE>{BASE64_DEL_ZIP}</xDE>
</rEnvioLote>
</env:Body>
</env:Envelope>
Respuesta → env:Body > hijo con prefijo ns2 → rResEnviLoteDe
Consulta de Lote:
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Header/>
<env:Body>
<rEnviConsLoteDe xmlns="http://ekuatia.set.gov.py/sifen/xsd">
<dId>{ID_TRANSACCION}</dId>
<dProtConsLote>{NUMERO_DE_LOTE}</dProtConsLote>
</rEnviConsLoteDe>
</env:Body>
</env:Envelope>
Respuesta → env:Body > hijo con prefijo ns2 → rResEnviConsLoteDe
Eventos (Cancelación / Inutilización):
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:xsd="http://ekuatia.set.gov.py/sifen/xsd">
<env:Header/>
<env:Body>
<xsd:rEnviEventoDe>
<xsd:dId>{ID}</xsd:dId>
<xsd:dEvReg>
{XML_DEL_EVENTO_FIRMADO_SIN_DECLARACION}
</xsd:dEvReg>
</xsd:rEnviEventoDe>
</env:Body>
</env:Envelope>
Respuesta → env:Body > hijo con prefijo ns2 → rRetEnviEventoDe
3.3 Lote ZIP – Estructura interna
El lote es un archivo ZIP creado en memoria con un único archivo 1.xml:
<rLoteDE>
{XML_FIRMADO_SIN_DECLARACION}
</rLoteDE>
El ZIP completo se convierte a Base64 y viaja dentro de <xDE>.
3.4 mTLS – Autenticación de cliente
Todas las llamadas a SIFEN requieren mTLS (certificado cliente en la conexión TLS). Parámetros cURL obligatorios:
CURLOPT_SSLCERT→ ruta del archivo.p12CURLOPT_SSLCERTPASSWD→ contraseña del P12CURLOPT_SSLCERTTYPE→'P12'
En PHP 8.x+ la verificación SSL debe estar activa. El código legado la desactivaba solo para PHP < 8.0.
4. Lógica de Eventos: Cancelación vs. Inutilización
4.1 Diferencia técnica fundamental
Ambas operaciones usan el mismo endpoint (evento.wsdl) y el mismo sobre SOAP (rEnviEventoDe).
La diferencia está en el nodo hijo dentro de <gGroupTiEvt>:
| Operación | Nodo XML en gGroupTiEvt | Descripción DNIT |
|---|---|---|
| Cancelación | <rGeVeCan> | Anula un DE ya aprobado. Requiere el CDC del documento a cancelar y el motivo. |
| Inutilización | <rGesInut> | Marca como inutilizados números de documentos que nunca llegaron a emitirse (rangos de numeración). |
4.2 Estructura del XML de Cancelación
<gGroupGesEve xmlns="http://ekuatia.set.gov.py/sifen/xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ekuatia.set.gov.py/sifen/xsd siRecepEvento_v150.xsd">
<rGesEve xsi:schemaLocation="http://ekuatia.set.gov.py/sifen/xsd siRecepEvento_v150.xsd">
<rEve Id="1">
<dFecFirma>{YYYY-MM-DDTHH:mm:ss}</dFecFirma>
<dVerFor>150</dVerFor>
<gGroupTiEvt>
<rGeVeCan>
<Id>{CDC_DEL_DOCUMENTO_A_CANCELAR}</Id>
<mOtEve>{MOTIVO_TEXTO_LIBRE}</mOtEve>
</rGeVeCan>
</gGroupTiEvt>
</rEve>
</rGesEve>
</gGroupGesEve>
Proceso de firma para eventos:
- Todo el XML de evento se construye como string completo (incluyendo el SOAP Envelope).
- Se firma el nodo
<rEve Id="1">. - La firma
<Signature>se inserta como hijo de<rGesEve>. - El XML firmado completo (Envelope + evento firmado) se envía tal cual como POST body.
A diferencia del DE, el evento incluye el SOAP Envelope dentro del string que se firma y envía (no se embebe el XML firmado en un wrapper SOAP posterior).
4.3 Inutilización (no implementada en el legado, estructura esperada)
La inutilización usa <rGesInut> en lugar de <rGeVeCan>, con campos para:
- Tipo de Documento
- Establecimiento y Punto de expedición
- Rango de numeración (
dNumIniFoliydNumFinFoli) - Motivo
5. Lecciones Aprendidas – Fixes Técnicos Críticos
Fix #1 – Prefijo vacío en XMLSecurityDSig (BLOQUEANTE)
Síntoma: La DNIT rechaza con error de validación de firma.
Causa: Si se instancia XMLSecurityDSig('ds'), genera <ds:Signature>. La DNIT espera la firma sin prefijo de namespace.
Solución: Instanciar siempre con new XMLSecurityDSig('').
Fix #2 – Eliminación de la declaración XML (BLOQUEANTE)
Síntoma: El SOAP Envelope resultante es XML inválido o la DNIT lo rechaza.
Causa: DOMDocument::saveXML() agrega <?xml version="1.0" encoding="utf-8"?> al inicio. Al embeber este string dentro del Envelope SOAP, hay dos declaraciones XML.
Solución: Remover la declaración XML del resultado firmado antes de insertarlo en el Envelope.
Hay dos variantes a remover:
<?xml version="1.0" encoding="utf-8"?>(con encoding)<?xml version="1.0"?>(sin encoding)
Fix #3 – El QR se agrega fuera del nodo firmado (ARQUITECTURAL)
Síntoma: Si el QR se agrega antes de firmar, la validación del DigestValue falla.
Causa: El nodo <gCamFuFD> con el QR debe estar fuera del <DE> firmado.
Flujo correcto:
- Generar XML con
<rDE>que contiene<DE>. - Firmar: la referencia cubre solo el
<DE>. - Extraer el
DigestValuede la firma recién generada. - Construir la URL del QR usando ese
DigestValue. - Agregar
<gCamFuFD>como hermano del<DE>dentro de<rDE>.
Fix #4 – mTLS es obligatorio en todas las llamadas (BLOQUEANTE)
Síntoma: HTTP 403 o rechazo de conexión sin mensaje de error útil. Causa: SIFEN valida la identidad del llamador por certificado de cliente en la capa TLS, independientemente del contenido firmado del XML. Solución: Siempre configurar el certificado P12 en el cliente HTTP (cURL o equivalente en Guzzle/Laravel HTTP Client). En Laravel/Guzzle:
'cert' => ['/ruta/al/certificado.p12', 'contraseña'],
'ssl_key' => [...],
o usar la opción CURLOPT_SSLCERTTYPE = 'P12' directamente.
Fix #5 – Namespace en el XML del evento (SUTIL)
Síntoma: El evento SOAP se envía pero la DNIT retorna error de estructura.
Causa: El XML del evento tiene un patrón de namespaces mixto: el Envelope usa el prefijo env: y xsd:, pero el body del evento (gGroupGesEve) vuelve a declarar el namespace SIFEN sin prefijo con xmlns=.
Solución: Respetar exactamente la estructura de namespace del legado:
env:Envelopeconxmlns:env="http://www.w3.org/2003/05/soap-envelope"yxmlns:xsd="http://ekuatia.set.gov.py/sifen/xsd"gGroupGesEveconxmlns="http://ekuatia.set.gov.py/sifen/xsd"
Fix #6 – Parseo de respuesta con namespace ns2 (IMPLEMENTACIÓN)
Síntoma: La respuesta SOAP no se puede parsear correctamente.
Causa: SIFEN retorna el body con el namespace http://ekuatia.set.gov.py/sifen/xsd mapeado al prefijo ns2 en la respuesta.
Solución en SimpleXML:
$xml = new SimpleXMLElement($response, 0, false, 'env', true);
$resultado = $xml->Body->children('ns2', true)->rRetEnviDe;
En Laravel/PHP moderno, usar DOMXPath con registerNamespace('ns2', 'http://ekuatia.set.gov.py/sifen/xsd').
Fix #7 – Opción overwrite => false en addReference (INTEGRIDAD)
Síntoma: El atributo URI en <Reference> queda vacío o incorrecto.
Causa: Por defecto, xmlseclibs puede sobrescribir el atributo URI.
Solución: Pasar ['overwrite' => false] como cuarto argumento de addReference() para preservar el URI original que apunta al ID del elemento firmado.
Fix #8 – Header HTTP exacto (SUTIL)
Content-Type obligatorio: application/xml; charset=utf-8
No usar text/xml ni application/soap+xml. La DNIT puede rechazar o no procesar correctamente con otros content-types.
6. Referencia Rápida – Namespace del Documento Electrónico
El elemento raíz del DE firmado debe declarar exactamente estos namespaces:
<rDE xmlns="http://ekuatia.set.gov.py/sifen/xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ekuatia.set.gov.py/sifen/xsd siRecepDE_v150.xsd">
- Versión del formato:
<dVerFor>150</dVerFor>(corresponde a la v1.5.0 del schema) - Schema de eventos:
siRecepEvento_v150.xsd - Schema de DE:
siRecepDE_v150.xsd
7. Checklist de Implementación en Laravel 12
-
CdcService– Construcción de la cadena de 43 dígitos con validaciones de ancho fijo -
Modulo11Service– Algoritmo DV para CDC (versión CDC) y para RUC (versión 7..2) -
XmlDeBuilder– Construcción del XML usandoDOMDocument(no heredocs concatenados) -
XmlSignerService– Wrapper sobre xmlseclibs con los parámetros exactos documentados en §2 -
QrCodeBuilder– Construcción del QR después de obtener elDigestValuede la firma -
SifenHttpClient– Cliente HTTP con mTLS configurado (Guzzle + cert P12) -
SifenSoapEnvelopeBuilder– Constructores de los 4 tipos de envelope documentados en §3 -
EventoCancelacionBuilder– Construcción del XML de evento conrGeVeCan -
EventoInutilizacionBuilder– Construcción del XML de evento conrGesInut - Variables de entorno para: rutas de certificados, contraseñas,
IdCSC,CSC, modo TEST/PROD
Documento generado el 2026-03-03 a partir del análisis de sifen_phplegacy.