Firma Digital
SUNAT exige que todos los documentos electrónicos estén firmados digitalmente con un certificado X.509 válido. openUBL expone un endpoint dedicado para firmar cualquier XML UBL previamente generado.
Requisitos
Sección titulada «Requisitos»- Certificado digital X.509 y clave privada en formato PEM (
.crt/.pem+.key/.pem), o - Certificado digital en formato PFX/P12 (
.pfxo.p12) protegido con su frase de clave. - El XML debe seguir la estructura UBL 2.1 de SUNAT
Conversión desde PFX/P12
Sección titulada «Conversión desde PFX/P12»Si posees un certificado en formato .pfx o .p12, también puedes usar la librería core para extraer PEM:
from openubl.signer import load_pfx
with open("certificado.pfx", "rb") as f: pfx_bytes = f.read()
key_pem, cert_pem = load_pfx(pfx_bytes, password="mi_clave")// La conversión de PFX a PEM se realiza con la librería Python openubl.// Alternativamente, envía el PFX directamente al endpoint /api/v1/sign.# La conversión de PFX a PEM se realiza con la librería Python openubl.# Alternativamente, envía el PFX directamente al endpoint /api/v1/sign.Endpoint
Sección titulada «Endpoint»POST /api/v1/sign| Campo | Tipo | Descripción |
|---|---|---|
cert_pem | string | Certificado X.509 en formato PEM (modo PEM) |
key_pem | string | Clave privada en formato PEM (modo PEM) |
pfx_base64 | string | Contenido del archivo PFX/P12 codificado en base64 estándar (modo PFX) |
pfx_password | string | Contraseña del archivo PFX/P12 (modo PFX) |
xml | string | XML UBL 2.1 a firmar |
Ejemplo con certificado PEM
Sección titulada «Ejemplo con certificado PEM»import requests
with open("certificado.pem", "r") as f: cert_pem = f.read()
with open("llave_privada.pem", "r") as f: key_pem = f.read()
xml_generado = "<?xml version='1.0' encoding='UTF-8'?>..."
response = requests.post( "http://localhost:8000/api/v1/sign", json={ "cert_pem": cert_pem, "key_pem": key_pem, "xml": xml_generado, },)
signed_xml = response.json()["signed_xml"]import { signXml } from "@openubl/sdk";import { zSignXmlBody } from "@openubl/sdk/zod.gen";
const body = zSignXmlBody.parse({cert_pem: "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----",key_pem: "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----",xml: "<?xml version="1.0" encoding="UTF-8"?>...",});
const { data, error } = await signXml({ body });
if (error) throw new Error(JSON.stringify(error));
console.log(data.signed_xml);curl -X POST "http://localhost:8000/api/v1/sign" \-H "Content-Type: application/json" \-d '{ "cert_pem": "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----", "key_pem": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----", "xml": "<?xml version="1.0" encoding="UTF-8"?>..."}'Ejemplo con certificado PFX
Sección titulada «Ejemplo con certificado PFX»import base64import requests
with open("certificado.pfx", "rb") as f: pfx_base64 = base64.b64encode(f.read()).decode("ascii")
xml_generado = "<?xml version='1.0' encoding='UTF-8'?>..."
response = requests.post( "http://localhost:8000/api/v1/sign", json={ "pfx_base64": pfx_base64, "pfx_password": "mi_clave", "xml": xml_generado, },)
signed_xml = response.json()["signed_xml"]import { readFile } from "fs/promises";import { signXml } from "@openubl/sdk";import { zSignXmlBody } from "@openubl/sdk/zod.gen";
const pfxBase64 = (await readFile("certificado.pfx")).toString("base64");
const body = zSignXmlBody.parse({pfx_base64: pfxBase64,pfx_password: "mi_clave",xml: "<?xml version="1.0" encoding="UTF-8"?>...",});
const { data, error } = await signXml({ body });
if (error) throw new Error(JSON.stringify(error));
console.log(data.signed_xml);PFX_BASE64=$(base64 -w 0 certificado.pfx)
curl -X POST "http://localhost:8000/api/v1/sign" \-H "Content-Type: application/json" \-d "{ \"pfx_base64\": \"$PFX_BASE64\", \"pfx_password\": \"mi_clave\", \"xml\": \"<?xml version=\"1.0\" encoding=\"UTF-8\"?>...\"}"Algoritmo de firma
Sección titulada «Algoritmo de firma»openUBL firma con XMLDSig (no XAdES-EPES), colocando la firma dentro de ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent, conforme a la estructura exigida por SUNAT en el Manual del Programador y las Reglas de validación actualizado al 24.04.2026.
Los algoritmos aplicados son los exigidos por la infraestructura de certificación peruana:
| Parámetro | URI / Valor |
|---|---|
| Canonicalización | http://www.w3.org/TR/2001/REC-xml-c14n-20010315 (C14N) |
| Digest | http://www.w3.org/2001/04/xmlenc#sha256 (SHA-256) |
| Firma | http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 (RSA-SHA-256) |
| Transform | Enveloped Signature |
Base normativa
Sección titulada «Base normativa»- RS N.° 300-2014/SUNAT y modificatorias: establecen la estructura XMLDSig y la ubicación de la firma en
UBLExtension/ExtensionContent. - Resolución de Secretaría N.° 007-2024-PCM/SGTD: aprueba la Directiva N.° 002-2024-PCM/SGTD, que regula el uso de la firma digital en entidades públicas y remite a estándares ETSI y a la Guía de Acreditación de Aplicaciones de Software de la IOFE/INDECOPI.
- INDECOPI/IOFE — Guía de Acreditación de Entidades de Certificación: exige algoritmos de firma de la familia SHA-2 (RSA-SHA-256/384/512 o ECDSA) para certificados digitales. La política de certificación de Entidades de Certificación acreditadas (2024) ya fija RSA 4096 bits — SHA-256 como configuración mínima.
Flujo completo
Sección titulada «Flujo completo»- Genera el XML con el endpoint correspondiente (
/invoice/create,/credit-note/create, etc.). - Envía el XML al endpoint
/api/v1/signcon tu certificado. - Recibe el XML firmado (
signed_xml) listo para enviar a SUNAT. `,