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#
| Activo | Por qué importa |
|---|---|
| Llave privada del CAF | Quien la tenga puede timbrar documentos con tus folios |
Certificado (.pfx/.p12) y su contraseña | Quien tenga ambos puede firmar documentos y hablar con el SII como tu firmante autorizado |
| Datos de los documentos | RUT, 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.
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
.pfxen 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 unCertificateopaco; reutilizarlo también permite quedte-sdkreutilice 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.
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ón | Necesita un secreto | Dónde ejecutarla |
|---|---|---|
createDte(), calculateTotals(), toXml() | No | En cualquier lugar, incluido el navegador |
parseCaf(), stampDte() | Llave privada del CAF | Servidor |
loadCertificate(), signDte() | Certificado y contraseña | Servidor |
sendToSii(), getStatus() | Certificado | Servidor |
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.
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, unSignedDteo la salida detoXml()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.
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#
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.
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'.
Decide por código, no por mensaje
Maneja DteError según su code. Reintenta SII_UNAVAILABLE; para todo
lo demás, corrige la causa.
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.
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.