Skip to content

Codigo 4102, El XML no cumple el esquema #1

@JeanOsorio

Description

@JeanOsorio

Buenas, estoy usando nodejs 20 con typescript para generar el xml usando verifactu-node-lib y y firmandolo antes de enviar a la AEAT, pero siempre me da ese error, te anexo lo que estoy haciendo a ver si me pueds echar una mano.

import {
  createVerifactuInvoice,
  Invoice as VfInvoice,
  Software as VfSoftware,
} from "verifactu-node-lib";

async function submitInvoice(req: Request, res: Response) {
      const { invoiceNumber } = req.params;
      const factura = await InvoiceModel.findOne({ invoiceNumber });
      if (!factura) {
        return res.status(404).json({ error: "Factura no encontrada" });
      }

      const { registro } = factura;

      /* 2. Configura software para la librería */
      const software: VfSoftware = {
        developerName: "Alba Centro de Bienestar Emocional SLP",
        developerIrsId: "B75935544",
        name: "centroalba",
        id: "AA",
        version: "1.0.0",
        number: "001",
        useOnlyVerifactu: true,
        useMulti: false,
        useCurrentMulti: false,
      };

      /* 3. Mapea a modelo de la librería */
      const invoice: VfInvoice = {
        issuer: {
          irsId: registro.IDEmisorFactura,
          name: "Alba Centro de Bienestar Emocional SLP",
        },
        recipient: {
          irsId: "00000000T",
          name: "Paciente anónimo",
          country: "ES",
        },
        id: {
          number: registro.NumSerieFactura,
          issuedTime: new Date(registro.FechaExpedicionFactura),
        },
        type: registro.TipoFactura as any,
        description: {
          text: "Consulta psicológica",
          operationDate: new Date(registro.FechaExpedicionFactura),
        },
        vatLines: [
          {
            vatOperation: "S1",
            base: +(parseFloat(registro.ImporteTotal) / 1.21).toFixed(2),
            rate: 21,
            amount: +registro.CuotaTotal,
            vatKey: "01",
          },
        ],
        total: +registro.ImporteTotal,
        amount: +registro.CuotaTotal,
      };

      /* 4. Genera RegistroAlta y huella */
      const {
        verifactuXml, // base64
        hash: huella,
      } = await createVerifactuInvoice(invoice, software, null, {}, true);

      factura.huella = huella;

      const registroAltaXML = Buffer.from(verifactuXml, "base64").toString();

      /* 5. Firma XAdES SOLO al <sf1:RegistroAlta> */
      const certPath = path.resolve(__dirname, "../../../certs/cert.pem");
      const keyPath = path.resolve(__dirname, "../../../certs/key-pkcs8.pem");
      const xmlFirmado = await firmarXades(registroAltaXML, certPath, keyPath); // Retorna RegistroAlta firmado
      factura.xmlFirmado = xmlFirmado;

      /* 6. Construye sobre SOAP */
      const soapEnvelope = buildSoapEnvelope(xmlFirmado, huella);
      factura.xmlSOAP = soapEnvelope;

      /* 7. Envía a VeriFACTU (entorno pruebas) */
      const httpsAgent = new https.Agent({
        cert: fs.readFileSync(certPath, "utf8"),
        key: fs.readFileSync(keyPath, "utf8"),
        minVersion: "TLSv1.2",
        maxVersion: "TLSv1.2",
        rejectUnauthorized: false,
      });

      const { data } = await axios.post(
        "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP",
        soapEnvelope,
        {
          httpsAgent,
          timeout: 20000,
          headers: {
            "Content-Type": "text/xml;charset=UTF-8",
            SOAPAction: "RegFactuSistemaFacturacion",
            "User-Agent": "VeriFACTU Client",
            Accept: "text/xml",
            "Cache-Control": "no-cache",
            Pragma: "no-cache",
          },
        },
      );
      console.log(data);

      /* 8. Guarda y responde */
      factura.estado = "enviada";
      factura.AEATResponse = data;
      await factura.save();

      res.json({ message: "Factura enviada a la AEAT (VeriFACTU).", huella });
}

mi metodo firmarXades hace lo siguiente:

import fs from "fs";
import * as xmldom from "xmldom";
import * as xadesjs from "xadesjs";
import { X509Certificate } from "@peculiar/x509";

export async function firmarXades(
  xmlStr: string,
  certPath: string,
  keyPath: string,
): Promise<string> {
  const cert = fs.readFileSync(certPath, "utf-8");
  const key = fs.readFileSync(keyPath, "utf-8");
  const xmlDoc = new xmldom.DOMParser().parseFromString(
    xmlStr,
    "application/xml",
  );

  const xml = new xadesjs.SignedXml();
  const cryptoKey = await xadesjs.Application.crypto.subtle.importKey(
    "pkcs8",
    Buffer.from(
      key.replace(/-----(BEGIN|END) PRIVATE KEY-----|\n/g, ""),
      "base64",
    ),
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    false,
    ["sign"],
  );

  const x509 = new X509Certificate(cert);

  // Validación: ¿caducado?
  if (x509.notAfter.getTime() < Date.now()) {
    throw new Error("El certificado está caducado");
  }

  // Convertir el certificado a base64
  let x509Base64 = "";
  if (x509.rawData) {
    x509Base64 = Buffer.from(x509.rawData).toString("base64");
  } else {
    x509Base64 = cert
      .replace("-----BEGIN CERTIFICATE-----", "")
      .replace("-----END CERTIFICATE-----", "")
      .replace(/\s+/g, "");
  }

  await xml.Sign(
    { name: "RSASSA-PKCS1-v1_5" },
    cryptoKey,
    xmlDoc.documentElement,
    {
      id: `xmldsig-${Date.now().toString()}`, // Añadir ID único para la firma
      x509: [x509Base64],
      references: [
        {
          id: `r-id-${Date.now().toString()}`,
          uri: "",
          transforms: ["enveloped"],
          hash: "SHA-256",
        },
      ],
      policy: {
        hash: "SHA-1",
        identifier: {
          value: "urn:oid:2.16.724.1.3.1.1.2.1.9",
          // @ts-ignore
          qualifiers: [
            {
              value:
                "https://sede.administracion.gob.es/politica_de_firma_anexo_1.pdf",
            },
          ],
        },
        digestValue: "ELVALUE=",
      },
    },
  );
  const signature = xml.XmlSignature.GetXml();
  xmlDoc.documentElement.appendChild(signature);

  return new xmldom.XMLSerializer().serializeToString(xmlDoc);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions