Saltar al contenido
dte-sdk

Seguridad

Emitir DTE implica custodiar dos tipos de secretos, las llaves privadas de los CAF y tu certificado digital, y procesar datos que identifican a empresas y personas. Esta página explica qué hace dte-sdk para protegerlos y cómo usarlo de forma defensiva.

¿Encontraste una vulnerabilidad? Repórtala de forma privada siguiendo la política de seguridad (SECURITY.md) del repositorio de dte-sdk, no en un issue público.

Qué estás protegiendo#

ActivoPor qué importa
Llave privada del CAFQuien la tenga puede timbrar documentos con tus folios
Certificado (.pfx/.p12) y su contraseñaQuien tenga ambos puede firmar documentos y hablar con el SII como tu firmante autorizado
Datos de los documentosRUT, razones sociales, direcciones, correos y montos de tus clientes y de tu empresa

Manejo de archivos CAF#

Un CAF contiene la llave privada que timbra cada documento de su rango, así que merece el mismo cuidado que una contraseña.

  • Guarda los CAF como secretos: en un gestor de secretos o un almacenamiento cifrado, nunca en tu repositorio, en un bucket público ni en una imagen de contenedor.
  • Cárgalos en el servidor, al momento de timbrar. stampDte() acepta el XML del CAF tal cual, así que no necesitas guardar una copia procesada en ningún otro lugar.
  • Registra los folios usados de forma transaccional, para que dos procesos no timbren el mismo folio con la misma llave.
  • Nunca envíes un CAF al navegador. Nada de crear o previsualizar un documento lo necesita.
TypeScript
import { parseCaf, stampDte } from 'dte-sdk'
 
// `secrets` stands for your secrets manager: not the repo, not the client.
const caf = parseCaf(await secrets.get('dte/caf/33'))
 
const stamped = stampDte(invoice, caf)

Manejo de certificados y contraseñas#

  • 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 sola filtración no entregue ambos.
  • Carga una vez y reutiliza el objeto. loadCertificate() devuelve un Certificate opaco; reutilizarlo también permite que dte-sdk reutilice el token del SII en caché.
  • La contraseña se queda privada. loadCertificate() nunca la registra en logs, nunca la guarda en el resultado y nunca la incluye en un error.
  • Anticípate al vencimiento. Los certificados vencen; anota la fecha de renovación para que la firma no se detenga un viernes en la tarde.
TypeScript
import { readFile } from 'node:fs/promises'
import { loadCertificate } from 'dte-sdk'
 
const certificate = await loadCertificate(
  await readFile(process.env.DTE_CERT_PATH!),
  process.env.DTE_CERT_PASSWORD!,
)

Nunca hagas commit de un CAF, un archivo .pfx o una contraseña de certificado, ni siquiera en un repositorio privado. Si alguno se filtra, dalo por comprometido: revoca el certificado con tu proveedor y deja de usar el CAF filtrado.

Firma en el servidor#

signDte() corre sobre Web Crypto, así que puede ejecutarse en el navegador. Eso no significa que deba: firmar en el navegador implica enviar la llave privada al navegador. Reparte el trabajo según lo que necesita cada función:

FunciónNecesita un secretoDónde ejecutarla
createDte(), calculateTotals(), toXml()NoEn cualquier lugar, incluido el navegador
parseCaf(), stampDte()Llave privada del CAFServidor
loadCertificate(), signDte()Certificado y contraseñaServidor
sendToSii(), getStatus()CertificadoServidor

Si quieres, crea y previsualiza los documentos en el cliente, y luego envía la entrada validada a tu servidor para timbrar, firmar y enviar. La excepción es una herramienta donde una persona firma con su propio certificado en su propio dispositivo y nada sale de ahí: en ese caso, firmar en el navegador deja la llave donde ya estaba.

Mensajes de error genéricos#

Todo mensaje de DteError es genérico. Nunca incluye RUT, montos, llaves del CAF ni contraseñas del certificado, así que nada sensible llega a tus logs, trazas o rastreador de errores a través de una excepción. Decide según error.code, nunca según el texto del mensaje.

TypeScript
import { createDte, DteError } from 'dte-sdk'
 
try {
  createDte(input)
} catch (error) {
  if (error instanceof DteError) {
    console.error(error.code)    // 'INVALID_RUT'
    console.error(error.message) // generic: the RUT itself is never included
  }
}

Datos personales fuera de los logs#

Un documento lleva datos personales y comerciales: RUT, nombres, direcciones, correos y montos. La ley chilena de protección de datos personales se aplica a ellos, y los logs son donde suelen filtrarse.

  • No registres entradas, documentos ni XML. Un Dte, un SignedDte o la salida de toXml() contienen todo lo anterior.
  • Registra identificadores que no sean personales: tipo de documento, folio, track id y código de error bastan para rastrear un documento.
  • No vuelvas a agregar el valor. Mantén tus propias líneas de log alrededor de una llamada fallida tan genéricas como el error.
TypeScript
logger.info('DTE sent', {
  type: signed.type,
  folio: signed.folio,
  trackId,
})
// Not: logger.info('DTE sent', signed). That logs RUTs, names and amounts.

Lista de verificación defensiva#

1

Mantén los secretos en el servidor

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

2

Elige el ambiente explícitamente

Lee environment desde la configuración, nunca lo deduzcas. Pasa cada tipo de documento nuevo por 'certification' antes de 'production'.

3

Decide por código, no por mensaje

Maneja DteError según su code. Reintenta SII_UNAVAILABLE; para todo lo demás, corrige la causa.

4

Acota las llamadas de red

Pasa un AbortSignal como AbortSignal.timeout(30_000) a sendToSii() para que un SII lento no bloquee un proceso indefinidamente.

5

Registra identificadores, no documentos

Tipo, folio, track id y código de error. Nunca RUT, nombres, montos ni XML.

Cadena de suministro#

dte-sdk tiene cero dependencias en runtime, así que el código que auditas es el código que ejecutas: unos 8,4 KB min+gzip para el bundle completo. La suite tiene 412 tests en 14 suites, todos pasando. Fija la versión en tu lockfile y revisa el changelog antes de actualizar, como harías con cualquier paquete que toca llaves de firma.

Reportar una vulnerabilidad#

Por favor, no abras un issue público por un problema de seguridad. Usa el reporte privado de vulnerabilidades de GitHub en el repositorio de dte-sdk, como se describe en su SECURITY.md, e incluye:

  • una descripción del problema y su impacto,
  • una reproducción mínima (función, entrada y resultado observado vs. esperado),
  • la versión afectada.

Nunca incluyas un CAF, certificado, contraseña ni datos de clientes reales en un reporte. Bastan fixtures o valores enmascarados.

Relacionado#