@devlas/dte-sii 2.12.0 → 2.12.2
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/CafSolicitor.js +18 -2
- package/dte-sii.d.ts +3 -0
- package/package.json +1 -1
- package/utils/pfx.js +57 -21
package/CafSolicitor.js
CHANGED
|
@@ -328,8 +328,24 @@ class CafSolicitor {
|
|
|
328
328
|
return { success: false, errorCode: 'UNKNOWN', error: 'No se obtuvo CAF en la respuesta' };
|
|
329
329
|
|
|
330
330
|
} catch (err) {
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
const msg = err.message || '';
|
|
332
|
+
const isUnknownCa = msg.includes('unknown ca') || msg.includes('CERT_UNTRUSTED') || msg.includes('unknown_ca');
|
|
333
|
+
const isCertExpired = msg.includes('certificate has expired') || msg.includes('CERT_HAS_EXPIRED');
|
|
334
|
+
const isSslError = isUnknownCa || isCertExpired || msg.includes('SSL') || msg.includes('TLS');
|
|
335
|
+
|
|
336
|
+
const errorCode = isUnknownCa ? 'SSL_CERT_CHAIN'
|
|
337
|
+
: isCertExpired ? 'SSL_CERT_EXPIRED'
|
|
338
|
+
: isSslError ? 'SSL_ERROR'
|
|
339
|
+
: 'NETWORK_ERROR';
|
|
340
|
+
|
|
341
|
+
const friendlyError = isUnknownCa
|
|
342
|
+
? 'SII rechazó el certificado PFX (cadena de CA incompleta — falta certificado intermedio).'
|
|
343
|
+
: isCertExpired
|
|
344
|
+
? 'El certificado digital PFX está vencido. El cliente debe renovarlo.'
|
|
345
|
+
: msg;
|
|
346
|
+
|
|
347
|
+
console.error(`[CafSolicitor] Error (${errorCode}): ${friendlyError}`);
|
|
348
|
+
return { success: false, errorCode, error: friendlyError };
|
|
333
349
|
}
|
|
334
350
|
}
|
|
335
351
|
|
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.
|
|
3
|
+
"version": "2.12.2",
|
|
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
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
|
75
|
-
const privateKey =
|
|
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
|
-
|
|
78
|
-
const
|
|
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(
|
|
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:
|
|
90
|
-
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
|
}
|