Saltar al contenido
dte-sdk

Seguridad

Emitir documentos tributarios electrónicos implica custodiar secretos que pueden firmar a nombre de tu empresa (un certificado digital, la llave privada de cada CAF) y procesar datos que identifican a empresas y personas. dte-sdk está hecho para que sus propios mensajes y logs no los filtren, pero los secretos mismos los proteges tú. Esta página dice cuáles son, dónde guardarlos, qué tacha y qué no tacha el SDK, y qué operaciones no se pueden deshacer.

¿Encontraste una vulnerabilidad? No abras un issue público. Repórtala en privado desde la pestaña Security del repositorio de dte-sdk (elige "Report a vulnerability"). La política completa está en SECURITY.md. Ver Reportar una vulnerabilidad.

Qué estás protegiendo#

SecretoPor qué importaDónde aparece
Certificado (.pfx o .p12) y su contraseñaQuien tiene ambos puede firmar documentos y hablar con el SII como el titular, e ingresar al portal como esa persona.loadCertificateFromPkcs12, loadCertificateFromPem; el Certificate que pasas al cliente del SII y al cliente del portal.
XML del CAFContiene la llave privada (RSASK) que timbra cada documento de su rango de folios. Quien lo tiene puede emitir documentos con tus folios.parseCaf; el CAF que obtienes de requestCaf; los archivos del almacén de certificación; los archivos CAF que findCafFiles encuentra en un directorio.
Estado y almacén de certificaciónEl directorio de state.json y sus artefactos guarda archivos CAF, así que guarda llaves privadas.createDirectoryCertificationStore({ dir }) de dte-sdk/portal.
Sesión del portalLas cookies de sesión son un inicio de sesión: quien las tiene actúa como la persona hasta que la sesión termina.El sessionCache del cliente del portal, y sus archivos si usas createDirectoryTokenCache.
Tokens del SIIUn token autentica envíos, consultas y reclamos durante cerca de una hora.El tokenCache del cliente del SII, y sus archivos si usas createDirectoryTokenCache.
XML firmado, capturas y archivos de envíosRUT, nombres, direcciones y montos de tu empresa y de tus clientes.capture, archive, el XML que guardes.

Los objetos son dato plano, y contienen las llaves#

Un Certificate es un objeto plano con la llave privada como string PEM (privateKeyPem). Un CAF interpretado también trae su privateKeyPem, y un SiiClient o un PortalClient contiene el certificado. Eso es cómodo y tiene una consecuencia: nunca serialices, registres ni envíes estos objetos. JSON.stringify(certificate), console.log(sii) o un rastreador de errores que capture variables locales imprimirán la llave.

La contraseña es distinta: los cargadores la usan para abrir el archivo y no la conservan, y CERT_PASSWORD_WRONG no trae detalles. Mantén el archivo y la contraseña separados, por ejemplo el .pfx en un almacén de secretos y la contraseña en una variable de entorno, para que una filtración no entregue ambos.

TypeScript
import { loadCertificateFromPkcs12 } from 'dte-sdk'
 
const certificate = loadCertificateFromPkcs12(
  await secrets.getBytes('dte/certificate.pfx'), // `secrets` representa tu gestor de secretos
  process.env.PFX_PASSWORD!,
)

Nunca subas al repositorio un CAF, un .pfx, una llave .pem ni una contraseña, ni siquiera a un repositorio privado. Si uno se filtra, trátalo como comprometido: pide a tu entidad certificadora que revoque el certificado y consulta al SII por el CAF. No esperes a ver si alguien lo usó.

Dónde guardarlos#

  • En un gestor de secretos o un almacén cifrado, nunca en el repositorio, en un bucket público, en una imagen de contenedor ni en el log de un build. Inyecta la contraseña como variable de entorno en tiempo de ejecución.
  • En el servidor. Firmar necesita la llave privada, así que corresponde al lado servidor. dte-sdk corre en el navegador, y eso está bien para armar y previsualizar documentos, que no necesitan secretos, pero poner un certificado o un CAF en un navegador es enviarle la llave. La excepción es una herramienta en que una persona firma con su propio certificado en su propio equipo y nada sale de ahí.
  • Cargar una vez y reutilizar. Carga el certificado al iniciar y pasa el mismo objeto; así la caché de tokens también reutiliza el token del SII.
  • Controlar los folios de forma transaccional, para que dos procesos nunca timbren el mismo folio. Perder el registro de folios repite folios ante el SII. createDirectoryFolioRegistry (dte-sdk/node) guarda su libro solo para el dueño (0600, en un directorio creado con 0700), porque tiene los RUT de las empresas y sus track ids.
  • Planificar el vencimiento. Un certificado vence (CERT_EXPIRED), y uno que todavía no está vigente también se rechaza (CERT_NOT_YET_VALID, con el día en que empieza en details.validFrom, el notBefore del certificado; allowExpired deja cargar ambos). Un CAF vence 6 meses después de su fecha de autorización (CAF_EXPIRED). isCertificateExpired y daysUntilExpiry permiten avisar antes de que deje de funcionar la firma.
FunciónNecesita un secretoDónde ejecutarla
createDocument, totales, createEnvelope, las funciones create… de los librosNoEn cualquier parte, incluido el navegador
parseCaf, stampDocument, issueDocumentLlave privada del CAFServidor
signDocument, signEnvelope, las funciones sign… de los librosCertificado y contraseñaServidor
El cliente del SII (dte-sdk)CertificadoServidor
El cliente del portal (dte-sdk/portal)Certificado, para TLS mutuoSolo Node: fetch no puede presentar un certificado cliente

El almacén de certificación#

createDirectoryCertificationStore({ dir }) escribe state.json (de forma atómica: archivo temporal y renombre) y los artefactos del proceso de certificación. Como el estado contiene llaves privadas de CAF, los directorios que crea son privados del usuario (modo 0700) y sus archivos ilegibles para otros (0600). Un directorio que ya existe conserva su modo. Está pensado para un solo proceso. Mantén el directorio fuera del control de versiones, fuera de respaldos menos protegidos que tu almacén de secretos y fuera de volúmenes compartidos.

La caché de tokens y sesiones en disco#

Los tokens y las sesiones del portal son credenciales: una sesión del portal guarda las cookies de sesión, y quien las tiene actúa como la persona hasta que la sesión termina. En memoria, mueren con el proceso. createDirectoryTokenCache({ dir }) de dte-sdk/node las guarda en disco, así un reinicio las reutiliza, y entonces el directorio también es un secreto.

  • Un archivo por clave, legible solo por su dueño (modo 0600, en un directorio creado con 0700; uno que ya existe conserva su modo). El archivo guarda también su clave, así dos claves nunca leen la entrada de la otra.
  • Se lee solo si es un archivo regular del mismo usuario. El archivo se abre sin bloquear y se revisa por el mismo manejador desde el que se lee; un dispositivo, una tubería, un directorio o un archivo que otro usuario dejó en el directorio nunca se lee.
  • En Windows deciden los permisos de dir. Allí los archivos no tienen modo de dueño y no se revisa el dueño.
  • Dale un directorio propio, fuera del repositorio, fuera de respaldos menos protegidos que tu almacén de secretos y fuera de volúmenes que otros comparten. No lo apuntes al directorio de la captura ni del archivo de envíos.
  • Nunca lanza, y nunca registra el token. Una lectura o escritura que falla es un fallo de caché, reportado a logger con la operación y el motivo, nunca con el token ni con el contenido del archivo.

Los archivos CAF que guardas en un directorio para findCafFiles y resolveCafFile (dte-sdk/node) contienen la llave privada del CAF (RSASK), así que ese directorio también es un secreto: trátalo como el almacén de certificación, fuera del control de versiones y de volúmenes compartidos. La búsqueda solo lee, pero abre cada archivo .xml bajo su dir, hasta cinco niveles de carpetas, así que no la apuntes a un directorio con más de lo que quieres buscar.

Qué mantiene el SDK fuera de errores y logs#

  • Errores. El mensaje de un DteError es una frase fija más unas pocas claves seguras (nombres de campo, códigos, números). Nunca contiene RUT, nombres, texto de ítems, URL ni cuerpos de respuesta. Eso vive en error.details, que debes tratar como sensible: puede traer el valor ofensivo (FIELD_TOO_LONG lo trae como value), un RUT, la URL que se llamó (una URL de estado lleva un RUT) o un extracto de respuesta. Registra error.code y las claves que necesites, no todo details. Ver Errores.
  • Extractos de respuesta. details.body de SII_HTTP_ERROR y de SII_RESPONSE_INVALID se corta a 2 000 caracteres y se tacha antes del corte, para que un token no pueda separarse de la etiqueta que lo identifica. El motivo de un fault SOAP también se tacha, y details.portalMessage del portal se lee de una página ya tachada, así que no trae un token ni un RSASK que mostró una página.
  • El logger. silentLogger es el valor por defecto. Si pasas console o tu propio logger, sus mensajes son strings fijos ('sii http', 'sii retrying') y su contexto trae nombres de operación, intentos, estados HTTP, códigos del SII y duraciones. Los mensajes nunca traen tokens, RUT ni XML. Una advertencia trae más: cuando un flujo de folios de dte-sdk/portal se corta y conserva lo que tiene, sii portal flow stopped, keeping what it holds trae los detalles del error, con cada detalle de texto tachado antes, así un token, un RSASK o una cookie de sesión llegan como [redacted]. Desde la 0.7.0 deja fuera lo que dijo una página (portalMessage, body) y la url de donde vino, que pueden nombrar a la empresa y su RUT; los errores lanzados los conservan. El resto de los detalles, la operation del paso, flow, code y stoppedAt se registran como antes; trata esa entrada con el mismo cuidado que details.
  • Contraseñas. La contraseña del certificado nunca se registra, se guarda ni se incluye en un error.

Lo que el SDK no puede hacer es proteger los valores que pones en tus propias líneas de log. Mantenlas tan genéricas como las del SDK:

TypeScript
logger.info('DTE sent', { type: 33, folio: 1234, trackId }) // identificadores que no son personales
// No: logger.info('DTE sent', document) // RUT, nombres, direcciones y montos

No registres documentos, XML firmado ni details completos. Tipo, folio, track id y código de error bastan para rastrear un documento.

Captura y archivo de envíos#

Ambos están apagados por defecto. Actívalos sabiendo qué conservan.

La captura (createDirectoryCapture, createMemoryCapture) registra cada intercambio HTTP para un ticket de soporte del SII. Antes de que el intercambio llegue al puerto, el cliente tacha: el contenido de los elementos TOKEN, SEMILLA y RSASK (crudos o escapados en XML, en cualquier caja, con o sin prefijo de espacio de nombres, incluso cuando el texto quedó cortado), las asignaciones TOKEN=, las cookies de sesión del portal y el conversationId, los bloques de llave privada PEM y los valores de las cabeceras Cookie, Set-Cookie, Authorization y Proxy-Authorization. Los cuerpos se cortan a 512 KiB. Los RUT y el XML no se tachan: el soporte del SII los necesita para diagnosticar un envío. Un directorio de capturas contiene datos personales y comerciales aunque no contenga ningún token.

El archivo de envíos (createDirectoryUploadArchive, createMemoryUploadArchive) conserva los bytes exactos de cada envío que el SII respondió, con el TrackID, los RUT de la empresa y de quien envía y la respuesta del SII (con tokens y semillas tachados). Ese es su propósito, y significa que el archivo contiene documentos firmados completos.

Los adaptadores de captura y de archivo de envíos escriben con los permisos por defecto de tu proceso, a diferencia del almacén de certificación y la caché de tokens. Crea tú el directorio con permisos restrictivos, mantenlo fuera del repositorio, decide cuánto tiempo lo conservas y protégelo como proteges los documentos mismos. El UploadReceipt.raw que devuelve uploadEnvelope es la respuesta del SII tal como llegó, sin tachar: no la registres a ciegas.

TLS#

La verificación TLS está activada por defecto, tanto en el transporte fetch como en el adaptador de Node. Déjala activada.

  • Una falla es TRANSPORT_TLS, y details.reason trae el código de OpenSSL o del sistema operativo (UNABLE_TO_GET_ISSUER_CERT_LOCALLY, por ejemplo), así puedes ver qué anda mal.
  • Un proxy corporativo o una CA privada se resuelve con createNodeHttpsTransport({ tls: { ca } }) de dte-sdk/node. ca se suma a las raíces públicas incluidas en Node, nunca las reemplaza.
  • rejectUnauthorized: false desactiva la verificación del certificado del servidor. Es el único interruptor que vuelve invisible a un atacante en el medio, y por esa conexión viajan un token, un archivo firmado o una sesión del portal. Úsalo solo para depurar contra un doble de pruebas local en tu propio equipo, nunca contra el SII y nunca en producción. Si aparece un error TLS contra un host real del SII, busca la causa (un almacén de confianza desactualizado, un proxy) en vez de silenciarlo.
  • legacyRenegotiation: true permite la renegociación legacy insegura. Ningún host del SII la necesita hoy.
  • El cliente del portal presenta tu certificado con TLS mutuo y verifica al servidor, con tope TLS 1.2, la configuración que se midió que funciona para sesiones autenticadas.

Operaciones que no se pueden deshacer#

Algunas funciones cambian estado real en el SII. El SDK no pide confirmación (es una biblioteca), así que el cuidado es tuyo. Las que lo necesitan:

OperaciónQué pasaCuidado
annulFolios (dte-sdk/portal)Anula folios sin usar. No se puede deshacer y pesa contra el cupo de folios seis meses. Para levantar un bloqueo de timbraje, emitir con los folios sin usar sirve y anular lo empeora.Exige un alcance explícito (range, withinDays o all); nunca pases uno más amplio de lo que quieres.
requestCaf y los flujos de foliosAutoriza folios reales. Un rango corto que queda sin usar baja el próximo máximo que otorga el SII.Pide lo que vas a usar; usa minQuantity. Los flujos de folios dejan la anulación apagada por defecto.
changePurchaseType, moveVatToCommonUse, moveVatToRecoverableEn producción cambian el registro de compras real. Un solo intento; si falla, el cambio pudo haberse aplicado.Relee el documento con fetchPurchaseDocument antes de reintentar.
declareComplianceLa declaración jurada de cumplimiento del representante legal. No se puede deshacer.Llámala solo cuando las muestras impresas estén aprobadas. El proceso de certificación la hace solo con declareCompliance: true.
declareReceiptsComplianceLa declaración de la postulación de boletas, que autoriza a la empresa a emitir boletas en producción. No se puede deshacer, se envía una vez.El proveedor es obligatorio y el representante es el dueño de la sesión.
downloadTestSetGenerar el set de pruebas asigna sus casos de nuevo.Llámala una vez por certificación.
registerClaimRegistra una aceptación o un reclamo sobre un documento recibido. Nunca se envía dos veces a propósito.Prueba primero en certificación.
Envíos en producciónUn documento que llegó al SII en producción es real.Un envío duplicado devuelve el TrackID original, pero un documento equivocado necesita una nota de crédito, no un segundo envío.

El portal además raciona las sesiones por RUT. Con el valor por defecto closePreviousSessions: true, cuando el SII informa el límite de sesiones y ofrece cerrar las anteriores, el cliente lo hace: una persona que tenga una sesión abierta en el portal con el mismo RUT queda desconectada. Desactívalo si no es lo que quieres.

Producción y certificación#

El cliente recibe un environment explícito: 'certification' (maullin) o 'production' (palena). Léelo desde la configuración y nunca lo infieras.

  • Certificación no es un entorno de pruebas para datos reales. Es el ambiente de pruebas del SII, pero entras con el mismo certificado real y el RUT real de tu empresa. Mantén las credenciales, los folios y los CAF de producción fuera de los ambientes de prueba.
  • Ejecuta primero todo flujo nuevo en certificación, y luego cambia el único ajuste.
  • El respaldo MIPYME lee producción desde cualquier cliente. downloadMipymeBackup no tiene host de certificación: lee documentos reales, emitidos o recibidos, incluso desde un cliente configurado para certificación. Un ambiente de desarrollo o de staging que guarde lo que descarga necesita su propia puerta.
  • Los folios y los CAF se obtienen por separado para cada ambiente. Mantén aparte los CAF de certificación y los de producción, y no los mezcles en un mismo almacén.

Mantener los datos reales fuera del repositorio#

El repositorio de dte-sdk es público y nunca debe contener a un contribuyente real. npm run scan es la guarda: falla si un archivo versionado en src/, tests/, scripts/, docs/, README.md, llms.txt o CHANGELOG.md contiene un RUT que no esté en una lista corta de RUT de documentación y constantes del SII, una ruta local de una máquina, o un archivo versionado que parezca un secreto (.pfx, .p12, .pem, .key, .env; los fixtures de prueba son la única excepción). Corre en CI y antes de cada publicación. Los ejemplos de esta documentación usan solo RUT ficticios. Haz lo mismo en tu propio repositorio: agrega certificados, CAF y archivos .env a .gitignore, y activa el escaneo de secretos en tu plataforma. Ver Contribuir.

Cadena de suministro#

  • Cuatro dependencias de runtime: node-forge, @xmldom/xmldom, fast-xml-parser y rut.ts. Están fijadas a versiones exactas. Las muestras impresas (dte-sdk/pdf) tienen dos peers opcionales, pdf-lib y bwip-js, que instalas solo si las usas.
  • Procedencia (provenance). Las versiones se publican con un tag y desde CI con npm provenance mediante npm Trusted Publishing (GitHub OIDC), así que no existe un NPM_TOKEN de larga vida que filtrar. El workflow se niega a publicar cuando el tag no coincide con la versión de package.json. Puedes verificar una copia instalada con npm audit signatures. Ver RELEASING.md.
  • Fija y revisa. Versiona tu lockfile, instala con npm ci y revisa las actualizaciones de dependencias, como con cualquier paquete que maneja llaves de firma.

Reportar una vulnerabilidad#

No abras un issue público: divulgar antes de que exista una corrección pone en riesgo a todos los usuarios.

  1. Ve a la pestaña Security de github.com/arrowsw/dte-sdk.
  2. Elige Report a vulnerability para abrir un aviso privado.
  3. Incluye una descripción y su impacto, una reproducción mínima (entrada, función llamada, resultado observado y esperado) y las versiones afectadas.

La política fija estas metas: acuse de recibo en 72 horas, evaluación y severidad en 7 días, y una corrección o mitigación tan rápida como lo amerite la severidad, coordinando contigo la divulgación. Mientras dte-sdk esté bajo la 1.0, solo la última versión 0.x recibe correcciones de seguridad. El texto completo es SECURITY.md.

Lista de verificación#

1

Los secretos se quedan en el servidor

Certificado, contraseña y CAF viven en un almacén de secretos y los carga solo el código que timbra, firma y envía.

2

Nunca serialices los objetos que contienen llaves

Ni JSON.stringify ni console.log de un certificado, un CAF, un SiiClient o un PortalClient.

3

Registra códigos, no detalles

Registra error.code y las claves que necesites. Mantén documentos, XML y details fuera de logs y rastreadores de errores.

4

Protege las capturas y los archivos

Contienen RUT y documentos completos. Permisos restrictivos, fuera del repositorio y con una decisión de retención.

5

Deja activada la verificación TLS

Usa tls.ca para una CA privada. Nunca pongas rejectUnauthorized: false en producción.

6

Prueba en certificación y luego cambia el ambiente

Fija el ambiente de forma explícita, y trata la anulación, las declaraciones y las escrituras al registro en producción como permanentes.