UNPKG

osodreamer-sri-xml-signer

Version:

Firma comprobantes XML con certificados digitales (.p12) para el SRI de Ecuador

434 lines (421 loc) 17.2 kB
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 });