osodreamer-sri-xml-signer
Version:
Firma comprobantes XML con certificados digitales (.p12) para el SRI de Ecuador
434 lines (421 loc) • 17.2 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
signXml: () => signXml
});
module.exports = __toCommonJS(index_exports);
// src/sign-xml/parsers/certificate-parser.ts
var forge4 = __toESM(require("node-forge"));
// src/sign-xml/strategies/banco-central.strategy.ts
var forge = __toESM(require("node-forge"));
var BancoCentralStrategy = class {
supports(friendlyName) {
return /BANCO CENTRAL/i.test(friendlyName);
}
getPrivateKey(bags) {
var _a;
const item = bags.find(
(bag) => {
var _a2, _b;
return /Signing Key/i.test((_b = (_a2 = bag.attributes) == null ? void 0 : _a2.friendlyName) == null ? void 0 : _b[0]);
}
);
if (!item)
throw new Error("No se encontr\xF3 la clave de firma para BANCO CENTRAL");
return (_a = item.key) != null ? _a : item.asn1;
}
overrideIssuerName(certBags) {
const cert = certBags[forge.pki.oids.certBag][0].cert;
return cert.issuer.attributes.reverse().map((attr) => `${attr.shortName}=${attr.value}`).join(",");
}
};
// src/sign-xml/strategies/security-data.strategy.ts
var forge2 = __toESM(require("node-forge"));
var SecurityDataStrategy = class {
supports(friendlyName) {
return /SECURITY DATA/i.test(friendlyName);
}
getPrivateKey(bags) {
var _a;
const item = bags[0];
if (!item) throw new Error("No se encontr\xF3 la clave en SECURITY DATA");
return (_a = item.key) != null ? _a : item.asn1;
}
overrideIssuerName(certBags) {
const cert = certBags[forge2.pki.oids.certBag][0].cert;
return cert.issuer.attributes.reverse().map((attr) => `${attr.shortName}=${attr.value}`).join(",");
}
};
// src/sign-xml/strategies/uanataca.strategy.ts
var forge3 = __toESM(require("node-forge"));
var UanatacaStrategy = class {
supports(friendlyName) {
return /UANATACA/i.test(friendlyName);
}
getPrivateKey(bags) {
var _a;
const item = bags[0];
if (!item) throw new Error("No se encontr\xF3 la clave en UANATACA");
return (_a = item.key) != null ? _a : item.asn1;
}
overrideIssuerName(certBags) {
const cert = certBags[forge3.pki.oids.certBag][0].cert;
return this.getX509IssuerName(cert);
}
hexEncodeUtf8(value) {
const utf8Bytes = forge3.util.encodeUtf8(value);
const hex = forge3.util.bytesToHex(utf8Bytes);
return `#0c${utf8Bytes.length.toString(16).padStart(2, "0")}${hex}`;
}
getX509IssuerName(cert) {
const attrs = cert.issuer.attributes;
const oidMap = {
"2.5.4.97": "2.5.4.97",
"2.5.4.3": "CN",
"2.5.4.11": "OU",
"2.5.4.10": "O",
"2.5.4.7": "L",
"2.5.4.6": "C"
};
const preferredOrder = [
"2.5.4.97",
"2.5.4.3",
"2.5.4.11",
"2.5.4.10",
"2.5.4.7",
"2.5.4.6"
];
return preferredOrder.map(
(oid) => attrs.find((attr) => attr.type === oid || attr.name === oid)
).filter(Boolean).map((attr) => {
const key = oidMap[attr.type] || attr.type;
if (attr.type === "2.5.4.97") {
return `${key}=${this.hexEncodeUtf8(attr.value)}`;
}
return `${key}=${attr.value}`;
}).join(",");
}
};
// src/sign-xml/factories/sign-strategy.factory.ts
var SignStrategyFactory = class {
constructor() {
this.strategies = [
new BancoCentralStrategy(),
new SecurityDataStrategy(),
new UanatacaStrategy()
];
}
getStrategy(friendlyName) {
const strategy = this.strategies.find((s) => s.supports(friendlyName));
if (!strategy) {
throw new Error(
`No existe estrategia para el certificado: ${friendlyName}`
);
}
return strategy;
}
};
// src/sign-xml/parsers/certificate-parser.ts
var CertificateParser = class {
constructor() {
this.strategyFactory = new SignStrategyFactory();
}
parse(p12Buffer, password) {
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
const uint8Array = new Uint8Array(p12Buffer);
const p12Base64 = forge4.util.binary.base64.encode(uint8Array);
const p12Decoded = forge4.util.decode64(p12Base64);
const p12Asn1 = forge4.asn1.fromDer(p12Decoded);
const p12 = forge4.pkcs12.pkcs12FromAsn1(p12Asn1, password);
const keyBags = p12.getBags({
bagType: forge4.pki.oids.pkcs8ShroudedKeyBag
});
const certBags = p12.getBags({ bagType: forge4.pki.oids.certBag });
const certificates = certBags[forge4.oids.certBag];
const friendlyName = (_i = (_c = (_b = (_a = certificates == null ? void 0 : certificates[1]) == null ? void 0 : _a.attributes) == null ? void 0 : _b.friendlyName) == null ? void 0 : _c[0]) != null ? _i : (_h = (_g = (_f = (_e = (_d = certificates == null ? void 0 : certificates[0]) == null ? void 0 : _d.cert) == null ? void 0 : _e.issuer) == null ? void 0 : _f.attributes) == null ? void 0 : _g[2]) == null ? void 0 : _h.value;
const strategy = this.strategyFactory.getStrategy(friendlyName);
const privateKey = strategy.getPrivateKey(
keyBags[forge4.oids.pkcs8ShroudedKeyBag]
);
const issuerName = strategy.overrideIssuerName(certBags);
const mainCertificate = certificates.reduce((prev, current) => {
return current.cert.extensions.length > prev.cert.extensions.length ? current : prev;
});
return {
privateKey,
certificate: mainCertificate.cert,
issuerName
};
}
};
// src/sign-xml/utils/crypto.util.ts
var forge5 = __toESM(require("node-forge"));
var CryptoUtils = class {
sha1_base64(txt) {
const md3 = forge5.md.sha1.create();
md3.update(txt);
const digestHex = md3.digest().toHex();
const digestBytes = forge5.util.hexToBytes(digestHex);
return forge5.util.encode64(digestBytes);
}
hexToBase64(str) {
let hex = ("00" + str).slice(0 - str.length - str.length % 2);
const binary = hex.replace(/\r|\n/g, "").replace(
/([\da-fA-F]{2}) ?/g,
(match) => String.fromCharCode(parseInt(match, 16))
);
return forge5.util.encode64(binary);
}
bigint3base64(bigint) {
const hex = bigint.toString(16).padStart(2, "0");
const binary = hex.match(/\w{2}/g).map((a) => String.fromCharCode(parseInt(a, 16))).join("");
const base64 = forge5.util.encode64(binary);
return base64.match(/.{1,76}/g).join("\n");
}
p_obtener_aleatorio() {
const arr = new Uint32Array(1);
crypto.getRandomValues(arr);
return 1e5 + arr[0] % 99e5;
}
};
// src/sign-xml/builders/signature-id-generator.ts
var SignatureIdGenerator = class {
constructor(crypto2 = new CryptoUtils()) {
this.crypto = crypto2;
}
next() {
return this.crypto.p_obtener_aleatorio();
}
all() {
return {
certificateNumber: this.next(),
signatureNumber: this.next(),
signedPropertiesNumber: this.next(),
signedInfoNumber: this.next(),
signedPropertiesIdNumber: this.next(),
referenceIdNumber: this.next(),
signatureValueNumber: this.next(),
ObjectNumber: this.next()
};
}
};
// src/sign-xml/builders/xades-builder.ts
var forge6 = __toESM(require("node-forge"));
var converter = __toESM(require("hex2dec"));
var utf8 = __toESM(require("utf8"));
var import_moment = __toESM(require("moment"));
var XadesBuilder = class {
constructor(crypto2 = new CryptoUtils()) {
this.crypto = crypto2;
}
build(certData, fileData) {
const { certificate, issuerName, privateKey } = certData;
const {
ObjectNumber,
certificateNumber,
referenceIdNumber,
signatureNumber,
signatureValueNumber,
signedInfoNumber,
signedPropertiesIdNumber,
signedPropertiesNumber
} = new SignatureIdGenerator().all();
let certificateX509_pem = forge6.pki.certificateToPem(certificate);
let certificateX509 = certificateX509_pem;
certificateX509 = certificateX509.substr(certificateX509.indexOf("\n"));
certificateX509 = certificateX509.substr(
0,
certificateX509.indexOf("\n-----END CERTIFICATE-----")
);
certificateX509 = certificateX509.replace(/\r?\n|\r/g, "").replace(/([^\0]{76})/g, "$1\n");
let certificateX509_asn1 = forge6.pki.certificateToAsn1(certificate);
let certificateX509_der = forge6.asn1.toDer(certificateX509_asn1).getBytes();
let certificateX509_der_hash = this.crypto.sha1_base64(certificateX509_der);
let dec = converter.hexToDec(certificate.serialNumber);
const X509SerialNumber = dec;
let exponent = this.crypto.hexToBase64(privateKey.e.data[0].toString(16));
let modulus = this.crypto.bigint3base64(privateKey.n);
const leer_xml = new TextDecoder("utf-8").decode(fileData);
let comprobante = leer_xml.replace(/\t|\r/g, "");
comprobante = comprobante.replace(/\n|\r/g, "");
const sha1_comprobante = this.crypto.sha1_base64(
utf8.encode(
comprobante.replace(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
""
)
)
);
const xmlns = 'xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#"';
let SignedProperties = "";
SignedProperties += '<etsi:SignedProperties Id="Signature' + signatureNumber + "-SignedProperties" + signedPropertiesNumber + '">';
SignedProperties += "<etsi:SignedSignatureProperties>";
SignedProperties += "<etsi:SigningTime>";
SignedProperties += (0, import_moment.default)().utcOffset(-5).format("YYYY-MM-DDTHH:mm:ssZ");
SignedProperties += "</etsi:SigningTime>";
SignedProperties += "<etsi:SigningCertificate>";
SignedProperties += "<etsi:Cert>";
SignedProperties += "<etsi:CertDigest>";
SignedProperties += '<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">';
SignedProperties += "</ds:DigestMethod>";
SignedProperties += "<ds:DigestValue>";
SignedProperties += certificateX509_der_hash;
SignedProperties += "</ds:DigestValue>";
SignedProperties += "</etsi:CertDigest>";
SignedProperties += "<etsi:IssuerSerial>";
SignedProperties += "<ds:X509IssuerName>";
SignedProperties += issuerName;
SignedProperties += "</ds:X509IssuerName>";
SignedProperties += "<ds:X509SerialNumber>";
SignedProperties += X509SerialNumber;
SignedProperties += "</ds:X509SerialNumber>";
SignedProperties += "</etsi:IssuerSerial>";
SignedProperties += "</etsi:Cert>";
SignedProperties += "</etsi:SigningCertificate>";
SignedProperties += "</etsi:SignedSignatureProperties>";
SignedProperties += "<etsi:SignedDataObjectProperties>";
SignedProperties += '<etsi:DataObjectFormat ObjectReference="#Reference-ID-' + referenceIdNumber + '">';
SignedProperties += "<etsi:Description>";
SignedProperties += "contenido comprobante";
SignedProperties += "</etsi:Description>";
SignedProperties += "<etsi:MimeType>";
SignedProperties += "text/xml";
SignedProperties += "</etsi:MimeType>";
SignedProperties += "</etsi:DataObjectFormat>";
SignedProperties += "</etsi:SignedDataObjectProperties>";
SignedProperties += "</etsi:SignedProperties>";
let SignedProperties_para_hash = SignedProperties.replace(
"<etsi:SignedProperties",
"<etsi:SignedProperties " + xmlns
);
const sha1_SignedProperties = this.crypto.sha1_base64(
utf8.encode(SignedProperties_para_hash)
);
let KeyInfo = "";
KeyInfo += '<ds:KeyInfo Id="Certificate' + certificateNumber + '">';
KeyInfo += "\n<ds:X509Data>";
KeyInfo += "\n<ds:X509Certificate>\n";
KeyInfo += certificateX509;
KeyInfo += "\n</ds:X509Certificate>";
KeyInfo += "\n</ds:X509Data>";
KeyInfo += "\n<ds:KeyValue>";
KeyInfo += "\n<ds:RSAKeyValue>";
KeyInfo += "\n<ds:Modulus>\n";
KeyInfo += modulus;
KeyInfo += "\n</ds:Modulus>";
KeyInfo += "\n<ds:Exponent>";
KeyInfo += exponent;
KeyInfo += "</ds:Exponent>";
KeyInfo += "\n</ds:RSAKeyValue>";
KeyInfo += "\n</ds:KeyValue>";
KeyInfo += "\n</ds:KeyInfo>";
let KeyInfo_para_hash = KeyInfo.replace(
"<ds:KeyInfo",
"<ds:KeyInfo " + xmlns
);
const sha1_certificado = this.crypto.sha1_base64(KeyInfo_para_hash);
let SignedInfo = "";
SignedInfo += '<ds:SignedInfo Id="Signature-SignedInfo' + signedInfoNumber + '">';
SignedInfo += '\n<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">';
SignedInfo += "</ds:CanonicalizationMethod>";
SignedInfo += '\n<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1">';
SignedInfo += "</ds:SignatureMethod>";
SignedInfo += '\n<ds:Reference Id="SignedPropertiesID' + signedPropertiesIdNumber + '" Type="http://uri.etsi.org/01903#SignedProperties" URI="#Signature' + signatureNumber + "-SignedProperties" + signedPropertiesNumber + '">';
SignedInfo += '\n<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">';
SignedInfo += "</ds:DigestMethod>";
SignedInfo += "\n<ds:DigestValue>";
SignedInfo += sha1_SignedProperties;
SignedInfo += "</ds:DigestValue>";
SignedInfo += "\n</ds:Reference>";
SignedInfo += '\n<ds:Reference URI="#Certificate' + certificateNumber + '">';
SignedInfo += '\n<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">';
SignedInfo += "</ds:DigestMethod>";
SignedInfo += "\n<ds:DigestValue>";
SignedInfo += sha1_certificado;
SignedInfo += "</ds:DigestValue>";
SignedInfo += "\n</ds:Reference>";
SignedInfo += '\n<ds:Reference Id="Reference-ID-' + referenceIdNumber + '" URI="#comprobante">';
SignedInfo += "\n<ds:Transforms>";
SignedInfo += '\n<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature">';
SignedInfo += "</ds:Transform>";
SignedInfo += "\n</ds:Transforms>";
SignedInfo += '\n<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">';
SignedInfo += "</ds:DigestMethod>";
SignedInfo += "\n<ds:DigestValue>";
SignedInfo += sha1_comprobante;
SignedInfo += "</ds:DigestValue>";
SignedInfo += "\n</ds:Reference>";
SignedInfo += "\n</ds:SignedInfo>";
let SignedInfo_para_firma = SignedInfo.replace(
"<ds:SignedInfo",
"<ds:SignedInfo " + xmlns
);
let md3 = forge6.md.sha1.create();
md3.update(SignedInfo_para_firma, "utf8");
let signature = btoa(privateKey.sign(md3)).match(/.{1,76}/g).join("\n");
let xades_bes = "";
xades_bes += "<ds:Signature " + xmlns + ' Id="Signature' + signatureNumber + '">';
xades_bes += "\n" + SignedInfo;
xades_bes += '\n<ds:SignatureValue Id="SignatureValue' + signatureValueNumber + '">\n';
xades_bes += signature;
xades_bes += "\n</ds:SignatureValue>";
xades_bes += "\n" + KeyInfo;
xades_bes += '\n<ds:Object Id="Signature' + signatureNumber + "-Object" + ObjectNumber + '">';
xades_bes += '<etsi:QualifyingProperties Target="#Signature' + signatureNumber + '">';
xades_bes += SignedProperties;
xades_bes += "</etsi:QualifyingProperties>";
xades_bes += "</ds:Object>";
xades_bes += "</ds:Signature>";
return comprobante.replace(/(<[^<]+)$/, xades_bes + "$1");
}
};
// src/sign-xml/utils/xml.util.ts
var import_fast_xml_parser = require("fast-xml-parser");
function assertValidXml(buffer) {
const content = new TextDecoder("utf-8").decode(buffer);
const validation = import_fast_xml_parser.XMLValidator.validate(content);
if (validation !== true) {
throw new Error("Invalid XML: " + JSON.stringify(validation));
}
}
// src/sign-xml/sign-xml.ts
async function signXml(cmd) {
const parser = new CertificateParser();
const builder = new XadesBuilder();
const { p12Buffer, password, xmlBuffer } = cmd;
assertValidXml(xmlBuffer);
const certData = parser.parse(p12Buffer, password);
const signedXml = builder.build(certData, xmlBuffer);
return signedXml;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
signXml
});