@devlas/dte-sii 2.5.0
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/BoletaService.js +109 -0
- package/CAF.js +173 -0
- package/CafSolicitor.js +380 -0
- package/Certificado.js +123 -0
- package/ConsumoFolio.js +376 -0
- package/DTE.js +399 -0
- package/EnviadorSII.js +1304 -0
- package/Envio.js +196 -0
- package/FolioRegistry.js +553 -0
- package/FolioService.js +703 -0
- package/LICENSE +27 -0
- package/LibroBase.js +134 -0
- package/LibroCompraVenta.js +205 -0
- package/LibroGuia.js +225 -0
- package/README.md +239 -0
- package/Signer.js +94 -0
- package/SiiCertificacion.js +1189 -0
- package/SiiPortalAuth.js +460 -0
- package/SiiSession.js +499 -0
- package/cert/BoletaCert.js +731 -0
- package/cert/CertFolioHelper.js +185 -0
- package/cert/CertRunner.js +2658 -0
- package/cert/ConfigLoader.js +133 -0
- package/cert/IntercambioCert.js +429 -0
- package/cert/LibroCompras.js +359 -0
- package/cert/LibroGuias.js +171 -0
- package/cert/LibroVentas.js +153 -0
- package/cert/MuestrasImpresas.js +676 -0
- package/cert/SetBase.js +321 -0
- package/cert/SetBasico.js +413 -0
- package/cert/SetCompra.js +472 -0
- package/cert/SetExenta.js +490 -0
- package/cert/SetGuia.js +283 -0
- package/cert/SetParser.js +1184 -0
- package/cert/SetsProvider.js +499 -0
- package/cert/Simulacion.js +521 -0
- package/cert/comunaOficina.js +460 -0
- package/cert/index.js +124 -0
- package/cert/types.js +330 -0
- package/dte-sii.d.ts +458 -0
- package/index.js +428 -0
- package/package.json +48 -0
- package/utils/c14n.js +275 -0
- package/utils/calculo.js +396 -0
- package/utils/config.js +276 -0
- package/utils/constants.js +302 -0
- package/utils/emisor.js +174 -0
- package/utils/endpoints.js +225 -0
- package/utils/error.js +235 -0
- package/utils/index.js +339 -0
- package/utils/logger.js +239 -0
- package/utils/pfx.js +203 -0
- package/utils/receptor.js +218 -0
- package/utils/referencia.js +169 -0
- package/utils/resolucion.js +119 -0
- package/utils/rut.js +169 -0
- package/utils/sanitize.js +124 -0
- package/utils/tokenCache.js +214 -0
- package/utils/xml.js +358 -0
- package/utils.js +4 -0
package/Certificado.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* Certificado Digital
|
|
5
|
+
*
|
|
6
|
+
* Maneja certificados PFX/P12 para firma electrónica
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const forge = require('node-forge');
|
|
10
|
+
const {
|
|
11
|
+
certError,
|
|
12
|
+
ERROR_CODES,
|
|
13
|
+
createScopedLogger,
|
|
14
|
+
loadPfxFromBuffer,
|
|
15
|
+
isCertificateExpired,
|
|
16
|
+
getDaysUntilExpiry,
|
|
17
|
+
} = require('./utils');
|
|
18
|
+
|
|
19
|
+
const log = createScopedLogger('Certificado');
|
|
20
|
+
|
|
21
|
+
class Certificado {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Buffer} pfxBuffer - Contenido del archivo .pfx/.p12
|
|
24
|
+
* @param {string} password - Contraseña del certificado
|
|
25
|
+
* @throws {DteSiiError} Si el certificado es inválido o la contraseña es incorrecta
|
|
26
|
+
*/
|
|
27
|
+
constructor(pfxBuffer, password) {
|
|
28
|
+
if (!pfxBuffer) {
|
|
29
|
+
throw certError('PFX buffer es requerido', ERROR_CODES.CERT_INVALID);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!password) {
|
|
33
|
+
throw certError('Password del certificado es requerido', ERROR_CODES.CERT_PASSWORD_WRONG);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Usar utilidad centralizada para cargar PFX
|
|
37
|
+
const pfxData = loadPfxFromBuffer(pfxBuffer, password);
|
|
38
|
+
|
|
39
|
+
this.cert = pfxData.certificate;
|
|
40
|
+
this.privateKey = pfxData.privateKey;
|
|
41
|
+
|
|
42
|
+
// Verificar expiración usando utilidad centralizada
|
|
43
|
+
if (isCertificateExpired(pfxData.notAfter)) {
|
|
44
|
+
throw certError(
|
|
45
|
+
`Certificado expirado el ${pfxData.notAfter.toISOString()}`,
|
|
46
|
+
ERROR_CODES.CERT_EXPIRED,
|
|
47
|
+
{ expiresAt: pfxData.notAfter }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Advertir si está próximo a expirar
|
|
52
|
+
const daysUntilExpiry = getDaysUntilExpiry(pfxData.notAfter);
|
|
53
|
+
if (daysUntilExpiry <= 30) {
|
|
54
|
+
log.warn(`⚠️ Certificado expira en ${daysUntilExpiry} días`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Usar datos extraídos por la utilidad centralizada
|
|
58
|
+
this.rut = pfxData.rut;
|
|
59
|
+
this.nombre = pfxData.cn;
|
|
60
|
+
|
|
61
|
+
log.log('📜 Subject fields:', Object.entries(pfxData.subject).map(([k, v]) => `${k}: ${v}`).join(', '));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getPrivateKeyPem() {
|
|
65
|
+
return forge.pki.privateKeyToPem(this.privateKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getCertificatePem() {
|
|
69
|
+
return forge.pki.certificateToPem(this.cert);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Alias para compatibilidad
|
|
73
|
+
getPrivateKeyPEM() {
|
|
74
|
+
return this.getPrivateKeyPem();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getCertificatePEM() {
|
|
78
|
+
return this.getCertificatePem();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Obtener certificado X509 en base64 (sin headers PEM)
|
|
83
|
+
*/
|
|
84
|
+
getCertificateBase64() {
|
|
85
|
+
const pem = this.getCertificatePem();
|
|
86
|
+
return pem
|
|
87
|
+
.replace(/-----BEGIN CERTIFICATE-----/g, '')
|
|
88
|
+
.replace(/-----END CERTIFICATE-----/g, '')
|
|
89
|
+
.replace(/[\r\n\s]/g, '');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Obtener Modulus de la clave pública en base64
|
|
94
|
+
*/
|
|
95
|
+
getModulus() {
|
|
96
|
+
let bytes = this.privateKey.n.toByteArray();
|
|
97
|
+
if (bytes[0] === 0) bytes = bytes.slice(1);
|
|
98
|
+
const str = bytes.map(b => String.fromCharCode(b < 0 ? b + 256 : b)).join('');
|
|
99
|
+
return forge.util.encode64(str);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Obtener Exponent de la clave pública en base64
|
|
104
|
+
*/
|
|
105
|
+
getExponent() {
|
|
106
|
+
let bytes = this.privateKey.e.toByteArray();
|
|
107
|
+
if (bytes[0] === 0) bytes = bytes.slice(1);
|
|
108
|
+
const str = bytes.map(b => String.fromCharCode(b < 0 ? b + 256 : b)).join('');
|
|
109
|
+
return forge.util.encode64(str);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Firmar datos con SHA1withRSA
|
|
114
|
+
*/
|
|
115
|
+
sign(data, encoding = 'utf8') {
|
|
116
|
+
const md = forge.md.sha1.create();
|
|
117
|
+
md.update(data, encoding);
|
|
118
|
+
const signature = this.privateKey.sign(md);
|
|
119
|
+
return forge.util.encode64(signature);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = Certificado;
|
package/ConsumoFolio.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* ConsumoFolio.js
|
|
5
|
+
*
|
|
6
|
+
* Genera RCOF (Resumen Consumo de Folios) para boletas electrónicas.
|
|
7
|
+
* Documento requerido por SII para informar los folios de boletas utilizados.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { DOMParser } = require('@xmldom/xmldom');
|
|
12
|
+
|
|
13
|
+
class ConsumoFolio {
|
|
14
|
+
constructor(certificado) {
|
|
15
|
+
this.certificado = certificado;
|
|
16
|
+
this.caratula = null;
|
|
17
|
+
this.documentos = {
|
|
18
|
+
39: [], // Boleta electrónica
|
|
19
|
+
41: [], // Boleta exenta electrónica
|
|
20
|
+
};
|
|
21
|
+
this.xml = null;
|
|
22
|
+
this.id = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Establecer documentos desde objetos DTE
|
|
27
|
+
*/
|
|
28
|
+
setDocumentos(tipo, documentos) {
|
|
29
|
+
if (tipo !== 39 && tipo !== 41) {
|
|
30
|
+
throw new Error('Tipo de documento debe ser 39 o 41');
|
|
31
|
+
}
|
|
32
|
+
this.documentos[tipo] = Array.isArray(documentos) ? documentos : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Agregar documento individual
|
|
37
|
+
*/
|
|
38
|
+
agregar(tipo, dte) {
|
|
39
|
+
if (tipo !== 39 && tipo !== 41) {
|
|
40
|
+
throw new Error('Tipo de documento debe ser 39 o 41');
|
|
41
|
+
}
|
|
42
|
+
this.documentos[tipo].push(dte);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Agregar documentos desde un EnvioBoleta parseado
|
|
47
|
+
* @param {Object} envioBoleta - Objeto con { Tipo39: [], Tipo41: [] }
|
|
48
|
+
*/
|
|
49
|
+
agregarDesdeEnvioBoleta(envioBoleta) {
|
|
50
|
+
if (envioBoleta.Tipo39) {
|
|
51
|
+
for (const doc of envioBoleta.Tipo39) {
|
|
52
|
+
this.agregar(39, doc);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (envioBoleta.Tipo41) {
|
|
56
|
+
for (const doc of envioBoleta.Tipo41) {
|
|
57
|
+
this.agregar(41, doc);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Establecer carátula del RCOF
|
|
64
|
+
*/
|
|
65
|
+
setCaratula(caratula) {
|
|
66
|
+
this.caratula = { ...caratula };
|
|
67
|
+
// Normalizar FchResol: DD-MM-YYYY → YYYY-MM-DD (xs:date)
|
|
68
|
+
if (this.caratula.FchResol) {
|
|
69
|
+
const m = String(this.caratula.FchResol).match(/^(\d{2})-(\d{2})-(\d{4})$/);
|
|
70
|
+
if (m) this.caratula.FchResol = `${m[3]}-${m[2]}-${m[1]}`;
|
|
71
|
+
}
|
|
72
|
+
this.caratula.FchInicio = this.caratula.FchInicio || this.getFechaEmisionInicial();
|
|
73
|
+
this.caratula.FchFinal = this.caratula.FchFinal || this.getFechaEmisionFinal();
|
|
74
|
+
// Generar TmstFirmaEnv si no viene (formato ISO 8601: YYYY-MM-DDTHH:MM:SS)
|
|
75
|
+
if (!this.caratula.TmstFirmaEnv) {
|
|
76
|
+
const now = new Date();
|
|
77
|
+
this.caratula.TmstFirmaEnv = now.toISOString().slice(0, 19);
|
|
78
|
+
}
|
|
79
|
+
this.id = `RCOF_${(caratula.RutEmisor || '').replace('-', '')}_${Date.now()}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getFechaEmisionInicial() {
|
|
83
|
+
const todas = [...this.documentos[39], ...this.documentos[41]];
|
|
84
|
+
if (todas.length === 0) return null;
|
|
85
|
+
|
|
86
|
+
const fechas = todas.map(d => {
|
|
87
|
+
if (d.Encabezado?.IdDoc?.FchEmis) return d.Encabezado.IdDoc.FchEmis;
|
|
88
|
+
if (d.FchEmis) return d.FchEmis;
|
|
89
|
+
return null;
|
|
90
|
+
}).filter(f => f);
|
|
91
|
+
|
|
92
|
+
if (fechas.length === 0) return null;
|
|
93
|
+
fechas.sort();
|
|
94
|
+
return fechas[0];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getFechaEmisionFinal() {
|
|
98
|
+
const todas = [...this.documentos[39], ...this.documentos[41]];
|
|
99
|
+
if (todas.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
const fechas = todas.map(d => {
|
|
102
|
+
if (d.Encabezado?.IdDoc?.FchEmis) return d.Encabezado.IdDoc.FchEmis;
|
|
103
|
+
if (d.FchEmis) return d.FchEmis;
|
|
104
|
+
return null;
|
|
105
|
+
}).filter(f => f);
|
|
106
|
+
|
|
107
|
+
if (fechas.length === 0) return null;
|
|
108
|
+
fechas.sort();
|
|
109
|
+
return fechas[fechas.length - 1];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Obtener resumen por tipo de documento
|
|
114
|
+
*/
|
|
115
|
+
getResumen() {
|
|
116
|
+
const resumenes = [];
|
|
117
|
+
|
|
118
|
+
for (const tipo of [39, 41]) {
|
|
119
|
+
const docs = this.documentos[tipo];
|
|
120
|
+
if (docs.length === 0) continue;
|
|
121
|
+
|
|
122
|
+
const resumen = {
|
|
123
|
+
TipoDocumento: tipo,
|
|
124
|
+
MntNeto: 0,
|
|
125
|
+
MntIva: 0,
|
|
126
|
+
TasaIVA: 19,
|
|
127
|
+
MntExento: 0,
|
|
128
|
+
MntTotal: 0,
|
|
129
|
+
FoliosEmitidos: docs.length,
|
|
130
|
+
FoliosAnulados: 0,
|
|
131
|
+
FoliosUtilizados: docs.length,
|
|
132
|
+
RangoUtilizados: this.getRangos(docs),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (const doc of docs) {
|
|
136
|
+
const totales = doc.Encabezado?.Totales || doc;
|
|
137
|
+
resumen.MntNeto += totales.MntNeto || 0;
|
|
138
|
+
resumen.MntIva += totales.IVA || 0;
|
|
139
|
+
resumen.MntExento += totales.MntExe || 0;
|
|
140
|
+
resumen.MntTotal += totales.MntTotal || 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Boleta exenta no tiene IVA
|
|
144
|
+
if (tipo === 41) {
|
|
145
|
+
resumen.MntNeto = 0;
|
|
146
|
+
resumen.MntIva = 0;
|
|
147
|
+
resumen.TasaIVA = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
resumenes.push(resumen);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return resumenes;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Obtener rangos de folios utilizados
|
|
158
|
+
*/
|
|
159
|
+
getRangos(docs) {
|
|
160
|
+
const folios = docs.map(d => {
|
|
161
|
+
if (d.Encabezado?.IdDoc?.Folio) return d.Encabezado.IdDoc.Folio;
|
|
162
|
+
if (d.Folio) return d.Folio;
|
|
163
|
+
return null;
|
|
164
|
+
}).filter(f => f !== null).sort((a, b) => a - b);
|
|
165
|
+
|
|
166
|
+
if (folios.length === 0) return [];
|
|
167
|
+
|
|
168
|
+
const rangos = [];
|
|
169
|
+
let inicio = folios[0];
|
|
170
|
+
let fin = folios[0];
|
|
171
|
+
|
|
172
|
+
for (let i = 1; i < folios.length; i++) {
|
|
173
|
+
if (folios[i] === fin + 1) {
|
|
174
|
+
fin = folios[i];
|
|
175
|
+
} else {
|
|
176
|
+
rangos.push({ Inicial: inicio, Final: fin });
|
|
177
|
+
inicio = folios[i];
|
|
178
|
+
fin = folios[i];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
rangos.push({ Inicial: inicio, Final: fin });
|
|
182
|
+
|
|
183
|
+
return rangos;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Generar XML del Consumo de Folios
|
|
188
|
+
* Genera XML SIN indentación para C14N consistente
|
|
189
|
+
*/
|
|
190
|
+
generar() {
|
|
191
|
+
if (!this.caratula) {
|
|
192
|
+
throw new Error('Debe establecer la carátula antes de generar');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const resumenes = this.getResumen();
|
|
196
|
+
|
|
197
|
+
// Construir XML de resúmenes SIN indentación (para C14N limpio)
|
|
198
|
+
let resumenXml = '';
|
|
199
|
+
for (const r of resumenes) {
|
|
200
|
+
resumenXml += '<Resumen>';
|
|
201
|
+
resumenXml += `<TipoDocumento>${r.TipoDocumento}</TipoDocumento>`;
|
|
202
|
+
|
|
203
|
+
if (r.MntNeto) resumenXml += `<MntNeto>${r.MntNeto}</MntNeto>`;
|
|
204
|
+
if (r.MntIva) resumenXml += `<MntIva>${r.MntIva}</MntIva>`;
|
|
205
|
+
if (r.TasaIVA) resumenXml += `<TasaIVA>${r.TasaIVA}</TasaIVA>`;
|
|
206
|
+
if (r.MntExento) resumenXml += `<MntExento>${r.MntExento}</MntExento>`;
|
|
207
|
+
|
|
208
|
+
resumenXml += `<MntTotal>${r.MntTotal}</MntTotal>`;
|
|
209
|
+
resumenXml += `<FoliosEmitidos>${r.FoliosEmitidos}</FoliosEmitidos>`;
|
|
210
|
+
resumenXml += `<FoliosAnulados>${r.FoliosAnulados}</FoliosAnulados>`;
|
|
211
|
+
resumenXml += `<FoliosUtilizados>${r.FoliosUtilizados}</FoliosUtilizados>`;
|
|
212
|
+
|
|
213
|
+
if (r.RangoUtilizados && r.RangoUtilizados.length > 0) {
|
|
214
|
+
for (const rango of r.RangoUtilizados) {
|
|
215
|
+
resumenXml += '<RangoUtilizados>';
|
|
216
|
+
resumenXml += `<Inicial>${rango.Inicial}</Inicial>`;
|
|
217
|
+
resumenXml += `<Final>${rango.Final}</Final>`;
|
|
218
|
+
resumenXml += '</RangoUtilizados>';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
resumenXml += '</Resumen>';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Construir XML SIN indentación para C14N consistente
|
|
226
|
+
const schemaLoc = 'http://www.sii.cl/SiiDte ConsumoFolio_v10.xsd';
|
|
227
|
+
const xmlSinFirma = `<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
228
|
+
<ConsumoFolios xmlns="http://www.sii.cl/SiiDte" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="${schemaLoc}" version="1.0"><DocumentoConsumoFolios ID="${this.id}"><Caratula version="1.0"><RutEmisor>${this.caratula.RutEmisor}</RutEmisor><RutEnvia>${this.caratula.RutEnvia}</RutEnvia><FchResol>${this.caratula.FchResol}</FchResol><NroResol>${this.caratula.NroResol}</NroResol><FchInicio>${this.caratula.FchInicio}</FchInicio><FchFinal>${this.caratula.FchFinal}</FchFinal><SecEnvio>${this.caratula.SecEnvio}</SecEnvio><TmstFirmaEnv>${this.caratula.TmstFirmaEnv}</TmstFirmaEnv></Caratula>${resumenXml}</DocumentoConsumoFolios></ConsumoFolios>`;
|
|
229
|
+
|
|
230
|
+
// Firmar el documento
|
|
231
|
+
this.xml = this.firmar(xmlSinFirma);
|
|
232
|
+
return this.xml;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Firmar el XML del Consumo de Folios
|
|
237
|
+
*
|
|
238
|
+
* Estrategia: usar C14N con namespaces heredados como PHP DOMElement::C14N()
|
|
239
|
+
*/
|
|
240
|
+
firmar(xml) {
|
|
241
|
+
const parser = new DOMParser();
|
|
242
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
243
|
+
|
|
244
|
+
// Obtener el elemento a firmar
|
|
245
|
+
const documentoConsumoFolios = doc.getElementsByTagName('DocumentoConsumoFolios')[0];
|
|
246
|
+
|
|
247
|
+
// Obtener namespace del padre (ConsumoFolios)
|
|
248
|
+
const root = doc.getElementsByTagName('ConsumoFolios')[0];
|
|
249
|
+
const nsDefault = root.getAttribute('xmlns') || 'http://www.sii.cl/SiiDte';
|
|
250
|
+
|
|
251
|
+
// Calcular C14N manual (incluye namespace heredado como PHP)
|
|
252
|
+
const c14nDocumento = this._c14nDocumentoConsumoFolios(documentoConsumoFolios, nsDefault);
|
|
253
|
+
|
|
254
|
+
console.log('🔍 C14N Length:', c14nDocumento.length);
|
|
255
|
+
console.log('🔍 C14N primeros 300 chars:', c14nDocumento.substring(0, 300));
|
|
256
|
+
|
|
257
|
+
// Calcular DigestValue = base64(sha1(C14N))
|
|
258
|
+
const digest = crypto.createHash('sha1').update(c14nDocumento).digest('base64');
|
|
259
|
+
console.log('🔍 DigestValue:', digest);
|
|
260
|
+
|
|
261
|
+
// Construir SignedInfo (con tags expandidos para firmar)
|
|
262
|
+
// PHP incluye xmlns:xsi en SignedInfo para ConsumoFolio
|
|
263
|
+
const signedInfoParaFirmar = `<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod><Reference URI="#${this.id}"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue>${digest}</DigestValue></Reference></SignedInfo>`;
|
|
264
|
+
|
|
265
|
+
// Firmar SignedInfo con RSA-SHA1
|
|
266
|
+
const sign = crypto.createSign('RSA-SHA1');
|
|
267
|
+
sign.update(signedInfoParaFirmar);
|
|
268
|
+
const signatureValue = sign.sign(this.certificado.getPrivateKeyPem(), 'base64');
|
|
269
|
+
const formattedSignatureValue = signatureValue.match(/.{1,76}/g).join('\n');
|
|
270
|
+
|
|
271
|
+
// Obtener datos del certificado
|
|
272
|
+
const certBase64 = this.certificado.getCertificatePem()
|
|
273
|
+
.replace('-----BEGIN CERTIFICATE-----', '')
|
|
274
|
+
.replace('-----END CERTIFICATE-----', '')
|
|
275
|
+
.replace(/\s/g, '');
|
|
276
|
+
const modulus = this.certificado.getModulus();
|
|
277
|
+
const exponent = this.certificado.getExponent();
|
|
278
|
+
|
|
279
|
+
// Construir Signature (con self-closing tags para guardar)
|
|
280
|
+
const signedInfoParaGuardar = `<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#${this.id}"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>${digest}</DigestValue></Reference></SignedInfo>`;
|
|
281
|
+
|
|
282
|
+
const signatureXml = `<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">${signedInfoParaGuardar}<SignatureValue>\n${formattedSignatureValue}</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>${modulus}</Modulus><Exponent>${exponent}</Exponent></RSAKeyValue></KeyValue><X509Data><X509Certificate>${certBase64}</X509Certificate></X509Data></KeyInfo></Signature>`;
|
|
283
|
+
|
|
284
|
+
// Insertar firma DESPUÉS de DocumentoConsumoFolios, ANTES del cierre de ConsumoFolios
|
|
285
|
+
const xmlFirmado = xml.replace('</DocumentoConsumoFolios></ConsumoFolios>', `</DocumentoConsumoFolios>${signatureXml}</ConsumoFolios>`);
|
|
286
|
+
|
|
287
|
+
return xmlFirmado;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Calcular C14N del DocumentoConsumoFolios
|
|
292
|
+
*
|
|
293
|
+
* PHP DOMElement::C14N() incluye TODOS los namespaces en ámbito (in-scope),
|
|
294
|
+
* no solo los que están en uso directo.
|
|
295
|
+
*/
|
|
296
|
+
_c14nDocumentoConsumoFolios(elemento, nsDefault) {
|
|
297
|
+
const id = elemento.getAttribute('ID');
|
|
298
|
+
|
|
299
|
+
// Construir apertura con AMBOS namespaces heredados (como PHP C14N)
|
|
300
|
+
let c14n = `<DocumentoConsumoFolios`;
|
|
301
|
+
c14n += ` xmlns="${nsDefault}"`;
|
|
302
|
+
c14n += ` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`;
|
|
303
|
+
c14n += ` ID="${id}">`;
|
|
304
|
+
|
|
305
|
+
// Serializar contenido recursivamente
|
|
306
|
+
for (let i = 0; i < elemento.childNodes.length; i++) {
|
|
307
|
+
const child = elemento.childNodes[i];
|
|
308
|
+
if (child.nodeType === 1) { // Element
|
|
309
|
+
c14n += this._serializeElementForC14N(child);
|
|
310
|
+
} else if (child.nodeType === 3) { // Text
|
|
311
|
+
c14n += this._escapeXmlText(child.nodeValue);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
c14n += '</DocumentoConsumoFolios>';
|
|
316
|
+
return c14n;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Serializar elemento para C14N (sin repetir namespaces del padre)
|
|
321
|
+
*/
|
|
322
|
+
_serializeElementForC14N(elemento) {
|
|
323
|
+
const tagName = elemento.tagName || elemento.nodeName;
|
|
324
|
+
let result = `<${tagName}`;
|
|
325
|
+
|
|
326
|
+
// Agregar atributos (excepto xmlns que ya está en el padre)
|
|
327
|
+
const attrs = [];
|
|
328
|
+
for (let i = 0; i < elemento.attributes.length; i++) {
|
|
329
|
+
const attr = elemento.attributes[i];
|
|
330
|
+
if (attr.name !== 'xmlns' && attr.name !== 'xmlns:xsi') {
|
|
331
|
+
attrs.push({ name: attr.name, value: attr.value });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Ordenar atributos alfabéticamente (requisito C14N)
|
|
335
|
+
attrs.sort((a, b) => a.name.localeCompare(b.name));
|
|
336
|
+
for (const attr of attrs) {
|
|
337
|
+
result += ` ${attr.name}="${this._escapeXmlAttr(attr.value)}"`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
result += '>';
|
|
341
|
+
|
|
342
|
+
// Serializar hijos
|
|
343
|
+
for (let i = 0; i < elemento.childNodes.length; i++) {
|
|
344
|
+
const child = elemento.childNodes[i];
|
|
345
|
+
if (child.nodeType === 1) {
|
|
346
|
+
result += this._serializeElementForC14N(child);
|
|
347
|
+
} else if (child.nodeType === 3) {
|
|
348
|
+
result += this._escapeXmlText(child.nodeValue);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
result += `</${tagName}>`;
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
_escapeXmlText(text) {
|
|
357
|
+
return text
|
|
358
|
+
.replace(/&/g, '&')
|
|
359
|
+
.replace(/</g, '<')
|
|
360
|
+
.replace(/>/g, '>');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_escapeXmlAttr(text) {
|
|
364
|
+
return text
|
|
365
|
+
.replace(/&/g, '&')
|
|
366
|
+
.replace(/</g, '<')
|
|
367
|
+
.replace(/>/g, '>')
|
|
368
|
+
.replace(/"/g, '"');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
getXML() {
|
|
372
|
+
return this.xml;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
module.exports = ConsumoFolio;
|