@devlas/dte-sii 2.12.0 → 2.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dte-sii.d.ts CHANGED
@@ -300,7 +300,10 @@ export interface PfxData {
300
300
  privateKey: object;
301
301
  certificate: object;
302
302
  privateKeyPem: string;
303
+ /** Leaf certificate PEM only — use for XML signing */
303
304
  certificatePem: string;
305
+ /** Full chain PEM (leaf → intermediates → root) — use for TLS mutual auth */
306
+ certificateChainPem: string;
304
307
  subject: Record<string, string>;
305
308
  rut: string;
306
309
  cn: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.12.0",
3
+ "version": "2.12.1",
4
4
  "description": "Facturación y boletas electrónicas para el SII de Chile. Genera, timbra, firma y envía DTEs, libros electrónicos y automatiza la certificación.",
5
5
  "main": "index.js",
6
6
  "types": "dte-sii.d.ts",
package/utils/pfx.js CHANGED
@@ -19,7 +19,8 @@ const { certError, ERROR_CODES } = require('./error');
19
19
  * @property {forge.pki.PrivateKey} privateKey - Clave privada
20
20
  * @property {forge.pki.Certificate} certificate - Certificado
21
21
  * @property {string} privateKeyPem - Clave privada en formato PEM
22
- * @property {string} certificatePem - Certificado en formato PEM
22
+ * @property {string} certificatePem - Certificado hoja en formato PEM (solo leaf, para firma XML)
23
+ * @property {string} certificateChainPem - Cadena completa PEM: hoja → intermedios → raíz (para TLS mTLS)
23
24
  * @property {Object} subject - Campos del subject del certificado
24
25
  * @property {string} rut - RUT extraído del certificado (si existe)
25
26
  * @property {string} cn - Common Name del certificado
@@ -40,7 +41,7 @@ function loadPfxFromBuffer(pfxBuffer, password) {
40
41
  }
41
42
 
42
43
  let p12Asn1, p12;
43
-
44
+
44
45
  try {
45
46
  p12Asn1 = forge.asn1.fromDer(pfxBuffer.toString('binary'));
46
47
  } catch (err) {
@@ -56,38 +57,74 @@ function loadPfxFromBuffer(pfxBuffer, password) {
56
57
  throw certError(`Error descifrando PFX: ${err.message}`, ERROR_CODES.CERT_INVALID, { originalError: err });
57
58
  }
58
59
 
59
- // Extraer certificado y clave privada
60
- const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
61
- const keyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag });
62
-
63
- const certBag = certBags[forge.pki.oids.certBag];
64
- const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag];
60
+ // Extraer todos los certificados y la clave privada
61
+ const certBagsRaw = p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag] || [];
62
+ let keyBagRaw = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[forge.pki.oids.pkcs8ShroudedKeyBag] || [];
63
+ // Fallback: keyBag sin cifrado (formato legacy)
64
+ if (!keyBagRaw.length) {
65
+ keyBagRaw = p12.getBags({ bagType: forge.pki.oids.keyBag })[forge.pki.oids.keyBag] || [];
66
+ }
65
67
 
66
- if (!certBag || !certBag.length) {
68
+ if (!certBagsRaw.length) {
67
69
  throw certError('No se encontró certificado en el archivo PFX', ERROR_CODES.CERT_INVALID);
68
70
  }
69
71
 
70
- if (!keyBag || !keyBag.length) {
72
+ if (!keyBagRaw.length) {
71
73
  throw certError('No se encontró clave privada en el archivo PFX', ERROR_CODES.CERT_INVALID);
72
74
  }
73
75
 
74
- const certificate = certBag[0].cert;
75
- const privateKey = keyBag[0].key;
76
+ const allCerts = certBagsRaw.map(b => b.cert).filter(Boolean);
77
+ const privateKey = keyBagRaw[0].key;
78
+
79
+ // ── Identificar el certificado hoja (leaf) ───────────────────────────────
80
+ // Estrategia 1: cert cuya clave pública coincide con la clave privada (RSA)
81
+ let leafCert = null;
82
+ try {
83
+ const pubFromKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e);
84
+ if (pubFromKey) {
85
+ const pubPem = forge.pki.publicKeyToPem(pubFromKey);
86
+ leafCert = allCerts.find(c => forge.pki.publicKeyToPem(c.publicKey) === pubPem) ?? null;
87
+ }
88
+ } catch (_) { /* EC u otro tipo de clave */ }
89
+
90
+ // Estrategia 2: primer cert cuyo basicConstraints no marca cA=true
91
+ if (!leafCert) {
92
+ leafCert = allCerts.find(c => {
93
+ const bc = c.getExtension('basicConstraints');
94
+ return !bc || bc.cA !== true;
95
+ }) ?? allCerts[0];
96
+ }
97
+
98
+ // ── Ordenar cadena: hoja → intermedios → raíz ────────────────────────────
99
+ // SII (palena.sii.cl) requiere la cadena completa en el TLS handshake mutuo.
100
+ // Sin los intermedios, el servidor devuelve SSL alert 48 (unknown_ca).
101
+ const remaining = allCerts.filter(c => c !== leafCert);
102
+ const chain = [leafCert];
103
+ let current = leafCert;
104
+ while (remaining.length > 0) {
105
+ const issuerHash = current.issuer.hash;
106
+ const idx = remaining.findIndex(c => c.subject.hash === issuerHash);
107
+ if (idx < 0) break;
108
+ current = remaining.splice(idx, 1)[0];
109
+ if (chain.includes(current)) break;
110
+ chain.push(current);
111
+ }
112
+ chain.push(...remaining);
76
113
 
77
- // Extraer información del subject
78
- const subject = extractSubjectFields(certificate);
79
- const rut = extractRutFromCertificate(certificate);
114
+ const subject = extractSubjectFields(leafCert);
115
+ const rut = extractRutFromCertificate(leafCert);
80
116
 
81
117
  return {
82
118
  privateKey,
83
- certificate,
119
+ certificate: leafCert,
84
120
  privateKeyPem: forge.pki.privateKeyToPem(privateKey),
85
- certificatePem: forge.pki.certificateToPem(certificate),
121
+ certificatePem: forge.pki.certificateToPem(leafCert),
122
+ certificateChainPem: chain.map(c => forge.pki.certificateToPem(c)).join(''),
86
123
  subject,
87
124
  rut,
88
125
  cn: subject.CN || null,
89
- notBefore: certificate.validity.notBefore,
90
- notAfter: certificate.validity.notAfter,
126
+ notBefore: leafCert.validity.notBefore,
127
+ notAfter: leafCert.validity.notAfter,
91
128
  };
92
129
  }
93
130
 
@@ -186,8 +223,7 @@ function getDaysUntilExpiry(notAfter) {
186
223
  function createTlsOptions(pfxData) {
187
224
  return {
188
225
  key: pfxData.privateKeyPem,
189
- cert: pfxData.certificatePem,
190
- certificate: pfxData.certificatePem,
226
+ cert: pfxData.certificateChainPem || pfxData.certificatePem,
191
227
  rejectUnauthorized: false,
192
228
  };
193
229
  }