Saltar al pie de página
USANDO IRONPDF

Cómo usar soluciones PDF API .NET con IronPDF

Firmas digitales PDF en ASP.NET Core para España: PAdES, XAdES, TicketBAI y LOPDGDD con IronPDF

El Royal Decree-Law 15/2025 obliga al software de facturación español a implementar VeriFactu antes del 1 de enero de 2027. Las haciendas forales del País Vasco (Bizkaia, Gipuzkoa, Araba) ya exigen TicketBAI con firma XAdES sobre cada XML de factura. La LOPDGDD exige que los documentos archivados mantengan su integridad verificable durante el periodo de retención. Para los ISVs que desarrollan software de facturación con ASP.NET Core, estas obligaciones convergen en un único requisito técnico: firma digital PAdES de los PDFs de factura con certificado FNMT.

Esta guía cubre la implementación completa en C#: desde la distinción entre PAdES (para el PDF) y XAdES (para el XML TicketBAI), hasta los patrones de producción para gestionar certificados FNMT, firmar en lote para el SII, y verificar la integridad del archivo conforme a eIDAS.


Por qué VeriFactu y TicketBAI cambian los requisitos de firma de su software

Antes de VeriFactu y TicketBAI, la firma digital en facturas españolas era una buena práctica. Con VeriFactu, pasa a ser una expectativa normativa: los PDFs de factura que incluyan la leyenda VERI*FACTU y el código QR de la AEAT son el documento de presentación de una transacción registrada en la cadena de custodia fiscal. La firma PAdES sobre ese PDF es la garantía de que no fue alterado tras el registro.

En el País Vasco, TicketBAI va más lejos. Cada factura requiere:

  1. Generación del XML TicketBAI con los datos del emisor, receptor e importes
  2. Firma del XML con XAdES (XML Advanced Electronic Signatures) usando certificado reconocido por la hacienda foral
  3. Envío del XML firmado a la hacienda correspondiente: bizkaia.eus (también programa BATUZ), gipuzkoa.eus o araba.eus
  4. Generación del PDF de presentación con el identificador TicketBAI devuelto por la hacienda
  5. Firma PAdES del PDF con IronPDF para archivado conforme a LOPDGDD y eIDAS

La firma XAdES del XML (paso 2) corresponde a bibliotecas de firma XML especializadas. IronPDF gestiona exclusivamente la firma PAdES del PDF de presentación (paso 5).


El marco normativo que determina qué tipo de firma usar

Marco Estándar de firma Aplica sobre Quién lo requiere
eIDAS (Reglamento UE) PAdES / XAdES / CAdES PDF / XML / cualquier fichero Cualquier documento con validez jurídica en la UE
LOPDGDD PAdES-LTV PDFs archivados con datos personales AEPD, auditorías de cumplimiento
VeriFactu PAdES (PDF), sin requisito explícito de firma en el PDF PDF de factura con QR AEAT y leyenda VERI*FACTU AEAT, software vendido en territorio AEAT
TicketBAI XAdES (XML) + PAdES opcional en PDF XML TicketBAI; PDF de presentación Bizkaia, Gipuzkoa y Araba (haciendas forales)
Facturae / FACe CAdES o XAdES (sobre el XML Facturae) XML Facturae 3.2.2 AAPP que reciben facturas B2G

Para la mayoría de los ISVs con clientes en territorio AEAT y País Vasco, el requisito práctico es: PAdES en el PDF con certificado FNMT.

Los niveles de PAdES y cuándo usar cada uno

PAdES-B cubre la firma básica. Válida para contratos y documentos inmediatos, pero no para archivado a largo plazo.

PAdES-T añade sello de tiempo (timestamp). Recomendable para facturas electrónicas que deben demostrar cuándo existió la firma.

PAdES-LTV incorpora datos de validación a largo plazo: respuestas OCSP, CRLs y cadena de certificados completa. Es el nivel recomendado para archivado bajo LOPDGDD: permite verificar la firma décadas después, incluso cuando el certificado FNMT original haya caducado.


Instalación de IronPDF en un proyecto ASP.NET Core

Install-Package IronPdf
using IronPdf;
using IronPdf.Signing;
using IronPdf;
using IronPdf.Signing;
Imports IronPdf
Imports IronPdf.Signing
$vbLabelText   $csharpLabel

Configure la clave de licencia al inicio de la aplicación en Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Clave de licencia desde configuración (no hardcodeada en código)
IronPdf.License.LicenseKey = builder.Configuration["IronPdf:LicenseKey"]
    ?? throw new InvalidOperationException("IronPDF license key not configured");

var app = builder.Build();
var builder = WebApplication.CreateBuilder(args);

// Clave de licencia desde configuración (no hardcodeada en código)
IronPdf.License.LicenseKey = builder.Configuration["IronPdf:LicenseKey"]
    ?? throw new InvalidOperationException("IronPDF license key not configured");

var app = builder.Build();
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.Extensions.Configuration

Dim builder = WebApplication.CreateBuilder(args)

' Clave de licencia desde configuración (no hardcodeada en código)
IronPdf.License.LicenseKey = If(builder.Configuration("IronPdf:LicenseKey"), Throw New InvalidOperationException("IronPDF license key not configured"))

Dim app = builder.Build()
$vbLabelText   $csharpLabel

IronPDF es compatible con .NET 8 LTS y .NET 9/10, adaptándose a los proyectos ASP.NET Core modernos.


Firma PAdES de facturas en ASP.NET Core: flujo VeriFactu

El caso de uso más habitual para ISVs españoles es generar un PDF de factura con IronPDF y firmarlo con PAdES antes de enviarlo al cliente o archivarlo. El siguiente ejemplo genera un PDF desde HTML e incluye la leyenda VERI*FACTU y la firma PAdES:

using IronPdf;
using IronPdf.Signing;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/factura/firmar", (IWebHostEnvironment env) =>
{
    // Generar el PDF de la factura desde plantilla HTML
    // La plantilla incluye leyenda VERI*FACTU y QR AEAT
    var renderer = new ChromePdfRenderer();
    renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;

    var documento = renderer.RenderHtmlAsPdf(@"
        <html lang='es'>
        <head><meta charset='UTF-8'></head>
        <body>
            <div style='display:flex; justify-content:space-between;'>
                <div>
                    <h2>Factura Electrónica F2024-001</h2>
                    <p>NIF: B12345678 | Fecha: 01/12/2024</p>
                </div>
                <div style='text-align:right;'>
                    <img src='data:image/png;base64,{QR_AEAT_BASE64}' 
                         style='width:80px;height:80px;' alt='QR AEAT' />
                    <p style='font-weight:bold; font-size:9pt;'>VERI*FACTU</p>
                    <p style='font-size:7pt;'>Factura verificable en la sede electrónica de la AEAT</p>
                </div>
            </div>
            <p>Total: 1.210,00 € (IVA incluido)</p>
        </body>
        </html>");

    // Ruta del certificado FNMT en el servidor
    // En producción: cargar desde variable de entorno, no desde sistema de ficheros
    string certPath = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx");

    // Firma PAdES conforme a eIDAS para archivado bajo LOPDGDD
    var firma = new PdfSignature(certPath, "contraseñaCertificado")
    {
        SigningContact = "facturacion@miempresa.com",
        SigningLocation = "Madrid, España",
        SigningReason = "Factura electrónica — VeriFactu/eIDAS/LOPDGDD"
    };

    documento.Sign(firma);

    string rutaSalida = Path.Combine(Path.GetTempPath(), "factura_firmada.pdf");
    documento.SaveAs(rutaSalida);

    return Results.File(rutaSalida, "application/pdf", "factura_firmada.pdf");
});

app.Run();
using IronPdf;
using IronPdf.Signing;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/factura/firmar", (IWebHostEnvironment env) =>
{
    // Generar el PDF de la factura desde plantilla HTML
    // La plantilla incluye leyenda VERI*FACTU y QR AEAT
    var renderer = new ChromePdfRenderer();
    renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;

    var documento = renderer.RenderHtmlAsPdf(@"
        <html lang='es'>
        <head><meta charset='UTF-8'></head>
        <body>
            <div style='display:flex; justify-content:space-between;'>
                <div>
                    <h2>Factura Electrónica F2024-001</h2>
                    <p>NIF: B12345678 | Fecha: 01/12/2024</p>
                </div>
                <div style='text-align:right;'>
                    <img src='data:image/png;base64,{QR_AEAT_BASE64}' 
                         style='width:80px;height:80px;' alt='QR AEAT' />
                    <p style='font-weight:bold; font-size:9pt;'>VERI*FACTU</p>
                    <p style='font-size:7pt;'>Factura verificable en la sede electrónica de la AEAT</p>
                </div>
            </div>
            <p>Total: 1.210,00 € (IVA incluido)</p>
        </body>
        </html>");

    // Ruta del certificado FNMT en el servidor
    // En producción: cargar desde variable de entorno, no desde sistema de ficheros
    string certPath = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx");

    // Firma PAdES conforme a eIDAS para archivado bajo LOPDGDD
    var firma = new PdfSignature(certPath, "contraseñaCertificado")
    {
        SigningContact = "facturacion@miempresa.com",
        SigningLocation = "Madrid, España",
        SigningReason = "Factura electrónica — VeriFactu/eIDAS/LOPDGDD"
    };

    documento.Sign(firma);

    string rutaSalida = Path.Combine(Path.GetTempPath(), "factura_firmada.pdf");
    documento.SaveAs(rutaSalida);

    return Results.File(rutaSalida, "application/pdf", "factura_firmada.pdf");
});

app.Run();
Imports IronPdf
Imports IronPdf.Signing
Imports Microsoft.AspNetCore.Mvc

Dim builder = WebApplication.CreateBuilder(args)
Dim app = builder.Build()

app.MapPost("/factura/firmar", Function(env As IWebHostEnvironment)
    ' Generar el PDF de la factura desde plantilla HTML
    ' La plantilla incluye leyenda VERI*FACTU y QR AEAT
    Dim renderer = New ChromePdfRenderer()
    renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4

    Dim documento = renderer.RenderHtmlAsPdf("
        <html lang='es'>
        <head><meta charset='UTF-8'></head>
        <body>
            <div style='display:flex; justify-content:space-between;'>
                <div>
                    <h2>Factura Electrónica F2024-001</h2>
                    <p>NIF: B12345678 | Fecha: 01/12/2024</p>
                </div>
                <div style='text-align:right;'>
                    <img src='data:image/png;base64,{QR_AEAT_BASE64}' 
                         style='width:80px;height:80px;' alt='QR AEAT' />
                    <p style='font-weight:bold; font-size:9pt;'>VERI*FACTU</p>
                    <p style='font-size:7pt;'>Factura verificable en la sede electrónica de la AEAT</p>
                </div>
            </div>
            <p>Total: 1.210,00 € (IVA incluido)</p>
        </body>
        </html>")

    ' Ruta del certificado FNMT en el servidor
    ' En producción: cargar desde variable de entorno, no desde sistema de ficheros
    Dim certPath As String = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx")

    ' Firma PAdES conforme a eIDAS para archivado bajo LOPDGDD
    Dim firma = New PdfSignature(certPath, "contraseñaCertificado") With {
        .SigningContact = "facturacion@miempresa.com",
        .SigningLocation = "Madrid, España",
        .SigningReason = "Factura electrónica — VeriFactu/eIDAS/LOPDGDD"
    }

    documento.Sign(firma)

    Dim rutaSalida As String = Path.Combine(Path.GetTempPath(), "factura_firmada.pdf")
    documento.SaveAs(rutaSalida)

    Return Results.File(rutaSalida, "application/pdf", "factura_firmada.pdf")
End Function)

app.Run()
$vbLabelText   $csharpLabel

ChromePdfRenderer convierte cualquier plantilla HTML válida a PDF con texto real seleccionable — esencial para que los sistemas de la AEAT puedan procesar el contenido. PdfSignature acepta la ruta del certificado FNMT y la contraseña; las propiedades opcionales (SigningContact, SigningLocation, SigningReason) añaden metadatos que los visores PDF muestran en el panel de firmas.

Devolver el PDF firmado desde memoria (sin fichero temporal)

Para APIs de alto rendimiento donde no se desea escribir ficheros temporales:

var stream = new MemoryStream();
documento.SaveAs(stream);
stream.Position = 0;
return Results.File(stream, "application/pdf", "factura_firmada.pdf");
var stream = new MemoryStream();
documento.SaveAs(stream);
stream.Position = 0;
return Results.File(stream, "application/pdf", "factura_firmada.pdf");
Dim stream As New MemoryStream()
documento.SaveAs(stream)
stream.Position = 0
Return Results.File(stream, "application/pdf", "factura_firmada.pdf")
$vbLabelText   $csharpLabel

Firma para TicketBAI: tres variantes forales en ASP.NET Core

TicketBAI exige gestionar las tres diputaciones forales del País Vasco de forma independiente. Cada una tiene su propia especificación de implementación y su propio portal de registro. El siguiente servicio ASP.NET Core contempla las tres variantes:

using IronPdf;
using IronPdf.Signing;
using Microsoft.AspNetCore.Mvc;

public class ServicioTicketBaiPdf
{
    private readonly string _rutaCertificado;
    private readonly string _passwordCertificado;

    public ServicioTicketBaiPdf(IConfiguration config)
    {
        // Cargar certificado desde configuración segura (no fichero en producción)
        _rutaCertificado = config["TicketBAI:CertPath"]!;
        _passwordCertificado = config["TicketBAI:CertPassword"]!;
    }

    // Genera y firma el PDF de presentación de una factura TicketBAI
    // El XML TicketBAI ya debe estar firmado con XAdES y registrado en la hacienda foral
    public byte[] GenerarPdfFacturaTicketBai(
        string htmlFactura,
        string provinciaForal,      // "Bizkaia", "Gipuzkoa" o "Araba"
        string identificadorTbai)   // ID asignado por la hacienda foral al registro
    {
        IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";

        // Añadir el identificador TicketBAI al pie de la factura
        string htmlConTbai = htmlFactura
            + $"<div class='pie-ticketbai'>" +
              $"<p>TicketBAI: {identificadorTbai} | " +
              $"Hacienda Foral: {ObtenerHaciendaForal(provinciaForal)}</p>" +
              "</div>";

        var renderer = new ChromePdfRenderer();
        renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;

        var pdf = renderer.RenderHtmlAsPdf(htmlConTbai);

        // Firma PAdES — configura el location con la provincia foral
        var firma = new PdfSignature(_rutaCertificado, _passwordCertificado)
        {
            SigningLocation = $"País Vasco — {provinciaForal}, España",
            SigningReason = $"Factura TicketBAI {provinciaForal} — conforme eIDAS"
        };

        pdf.Sign(firma);
        return pdf.BinaryData;
    }

    private static string ObtenerHaciendaForal(string provincia) => provincia switch
    {
        "Bizkaia"  => "Diputación Foral de Bizkaia (bizkaia.eus)",
        "Gipuzkoa" => "Diputación Foral de Gipuzkoa (gipuzkoa.eus)",
        "Araba"    => "Diputación Foral de Araba (araba.eus)",
        _          => throw new ArgumentException($"Provincia foral desconocida: {provincia}")
    };
}
using IronPdf;
using IronPdf.Signing;
using Microsoft.AspNetCore.Mvc;

public class ServicioTicketBaiPdf
{
    private readonly string _rutaCertificado;
    private readonly string _passwordCertificado;

    public ServicioTicketBaiPdf(IConfiguration config)
    {
        // Cargar certificado desde configuración segura (no fichero en producción)
        _rutaCertificado = config["TicketBAI:CertPath"]!;
        _passwordCertificado = config["TicketBAI:CertPassword"]!;
    }

    // Genera y firma el PDF de presentación de una factura TicketBAI
    // El XML TicketBAI ya debe estar firmado con XAdES y registrado en la hacienda foral
    public byte[] GenerarPdfFacturaTicketBai(
        string htmlFactura,
        string provinciaForal,      // "Bizkaia", "Gipuzkoa" o "Araba"
        string identificadorTbai)   // ID asignado por la hacienda foral al registro
    {
        IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";

        // Añadir el identificador TicketBAI al pie de la factura
        string htmlConTbai = htmlFactura
            + $"<div class='pie-ticketbai'>" +
              $"<p>TicketBAI: {identificadorTbai} | " +
              $"Hacienda Foral: {ObtenerHaciendaForal(provinciaForal)}</p>" +
              "</div>";

        var renderer = new ChromePdfRenderer();
        renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;

        var pdf = renderer.RenderHtmlAsPdf(htmlConTbai);

        // Firma PAdES — configura el location con la provincia foral
        var firma = new PdfSignature(_rutaCertificado, _passwordCertificado)
        {
            SigningLocation = $"País Vasco — {provinciaForal}, España",
            SigningReason = $"Factura TicketBAI {provinciaForal} — conforme eIDAS"
        };

        pdf.Sign(firma);
        return pdf.BinaryData;
    }

    private static string ObtenerHaciendaForal(string provincia) => provincia switch
    {
        "Bizkaia"  => "Diputación Foral de Bizkaia (bizkaia.eus)",
        "Gipuzkoa" => "Diputación Foral de Gipuzkoa (gipuzkoa.eus)",
        "Araba"    => "Diputación Foral de Araba (araba.eus)",
        _          => throw new ArgumentException($"Provincia foral desconocida: {provincia}")
    };
}
Imports IronPdf
Imports IronPdf.Signing
Imports Microsoft.AspNetCore.Mvc

Public Class ServicioTicketBaiPdf
    Private ReadOnly _rutaCertificado As String
    Private ReadOnly _passwordCertificado As String

    Public Sub New(config As IConfiguration)
        ' Cargar certificado desde configuración segura (no fichero en producción)
        _rutaCertificado = config("TicketBAI:CertPath")
        _passwordCertificado = config("TicketBAI:CertPassword")
    End Sub

    ' Genera y firma el PDF de presentación de una factura TicketBAI
    ' El XML TicketBAI ya debe estar firmado con XAdES y registrado en la hacienda foral
    Public Function GenerarPdfFacturaTicketBai(
        htmlFactura As String,
        provinciaForal As String,      ' "Bizkaia", "Gipuzkoa" o "Araba"
        identificadorTbai As String) As Byte()   ' ID asignado por la hacienda foral al registro

        IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY"

        ' Añadir el identificador TicketBAI al pie de la factura
        Dim htmlConTbai As String = htmlFactura _
            & $"<div class='pie-ticketbai'>" _
            & $"<p>TicketBAI: {identificadorTbai} | " _
            & $"Hacienda Foral: {ObtenerHaciendaForal(provinciaForal)}</p>" _
            & "</div>"

        Dim renderer As New ChromePdfRenderer()
        renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4

        Dim pdf = renderer.RenderHtmlAsPdf(htmlConTbai)

        ' Firma PAdES — configura el location con la provincia foral
        Dim firma As New PdfSignature(_rutaCertificado, _passwordCertificado) With {
            .SigningLocation = $"País Vasco — {provinciaForal}, España",
            .SigningReason = $"Factura TicketBAI {provinciaForal} — conforme eIDAS"
        }

        pdf.Sign(firma)
        Return pdf.BinaryData
    End Function

    Private Shared Function ObtenerHaciendaForal(provincia As String) As String
        Select Case provincia
            Case "Bizkaia"
                Return "Diputación Foral de Bizkaia (bizkaia.eus)"
            Case "Gipuzkoa"
                Return "Diputación Foral de Gipuzkoa (gipuzkoa.eus)"
            Case "Araba"
                Return "Diputación Foral de Araba (araba.eus)"
            Case Else
                Throw New ArgumentException($"Provincia foral desconocida: {provincia}")
        End Select
    End Function
End Class
$vbLabelText   $csharpLabel

Gestión del certificado FNMT en producción

Para software de facturación certificado bajo VeriFactu o TicketBAI, la gestión segura del certificado FNMT es crítica:

Variables de entorno (recomendado para la mayoría de los proyectos)

string certBase64 = Environment.GetEnvironmentVariable("FACTURACION_CERT_PFX")
    ?? throw new InvalidOperationException("Certificado FNMT no configurado.");

byte[] certBytes = Convert.FromBase64String(certBase64);
string certPassword = Environment.GetEnvironmentVariable("FACTURACION_CERT_PASSWORD")
    ?? throw new InvalidOperationException("Contraseña del certificado no configurada.");

var firma = new PdfSignature(certBytes, certPassword)
{
    SigningReason = "Factura electrónica — certificado FNMT/AEAT"
};
string certBase64 = Environment.GetEnvironmentVariable("FACTURACION_CERT_PFX")
    ?? throw new InvalidOperationException("Certificado FNMT no configurado.");

byte[] certBytes = Convert.FromBase64String(certBase64);
string certPassword = Environment.GetEnvironmentVariable("FACTURACION_CERT_PASSWORD")
    ?? throw new InvalidOperationException("Contraseña del certificado no configurada.");

var firma = new PdfSignature(certBytes, certPassword)
{
    SigningReason = "Factura electrónica — certificado FNMT/AEAT"
};
Imports System

Dim certBase64 As String = Environment.GetEnvironmentVariable("FACTURACION_CERT_PFX")
If certBase64 Is Nothing Then
    Throw New InvalidOperationException("Certificado FNMT no configurado.")
End If

Dim certBytes As Byte() = Convert.FromBase64String(certBase64)
Dim certPassword As String = Environment.GetEnvironmentVariable("FACTURACION_CERT_PASSWORD")
If certPassword Is Nothing Then
    Throw New InvalidOperationException("Contraseña del certificado no configurada.")
End If

Dim firma As New PdfSignature(certBytes, certPassword) With {
    .SigningReason = "Factura electrónica — certificado FNMT/AEAT"
}
$vbLabelText   $csharpLabel

Este patrón mantiene las credenciales fuera del control de versiones y simplifica la rotación del certificado cuando caduca: se actualiza la variable de entorno y se reinicia el servicio.

Azure Key Vault (para proyectos con infraestructura Azure)

Para proyectos en Azure, Azure Key Vault permite almacenar y usar el certificado FNMT sin que la clave privada salga del almacén. El SDK de .NET proporciona CertificateClient para acceder al certificado.


Firma en lote de facturas para VeriFactu y SII

El SII (Suministro Inmediato de Información) requiere que las grandes empresas comuniquen sus libros de registro a la AEAT. Para firmar lotes de facturas en el cierre mensual, cargue el certificado FNMT una sola vez fuera del bucle:

using IronPdf;
using IronPdf.Signing;

app.MapPost("/facturas/firmar-lote", (IWebHostEnvironment env) =>
{
    string certPath = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx");

    // Cargar certificado una sola vez — más eficiente para lotes grandes
    var firma = new PdfSignature(certPath, "contraseñaCert")
    {
        SigningReason = "Lote facturas mensual — VeriFactu/SII"
    };

    string[] rutasFacturas = Directory.GetFiles(
        Path.Combine(env.ContentRootPath, "FacturasPendientes"),
        "*.pdf"
    );

    string dirSalida = Path.Combine(env.ContentRootPath, "FacturasFirmadas");
    Directory.CreateDirectory(dirSalida);

    foreach (string rutaFactura in rutasFacturas)
    {
        var doc = PdfDocument.FromFile(rutaFactura);
        doc.Sign(firma);
        doc.SaveAs(Path.Combine(dirSalida, Path.GetFileName(rutaFactura)));
    }

    return Results.Ok(new { FacturasFirmadas = rutasFacturas.Length });
});
using IronPdf;
using IronPdf.Signing;

app.MapPost("/facturas/firmar-lote", (IWebHostEnvironment env) =>
{
    string certPath = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx");

    // Cargar certificado una sola vez — más eficiente para lotes grandes
    var firma = new PdfSignature(certPath, "contraseñaCert")
    {
        SigningReason = "Lote facturas mensual — VeriFactu/SII"
    };

    string[] rutasFacturas = Directory.GetFiles(
        Path.Combine(env.ContentRootPath, "FacturasPendientes"),
        "*.pdf"
    );

    string dirSalida = Path.Combine(env.ContentRootPath, "FacturasFirmadas");
    Directory.CreateDirectory(dirSalida);

    foreach (string rutaFactura in rutasFacturas)
    {
        var doc = PdfDocument.FromFile(rutaFactura);
        doc.Sign(firma);
        doc.SaveAs(Path.Combine(dirSalida, Path.GetFileName(rutaFactura)));
    }

    return Results.Ok(new { FacturasFirmadas = rutasFacturas.Length });
});
Imports IronPdf
Imports IronPdf.Signing

app.MapPost("/facturas/firmar-lote", Function(env As IWebHostEnvironment)
    Dim certPath As String = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx")

    ' Cargar certificado una sola vez — más eficiente para lotes grandes
    Dim firma As New PdfSignature(certPath, "contraseñaCert") With {
        .SigningReason = "Lote facturas mensual — VeriFactu/SII"
    }

    Dim rutasFacturas As String() = Directory.GetFiles(
        Path.Combine(env.ContentRootPath, "FacturasPendientes"),
        "*.pdf"
    )

    Dim dirSalida As String = Path.Combine(env.ContentRootPath, "FacturasFirmadas")
    Directory.CreateDirectory(dirSalida)

    For Each rutaFactura As String In rutasFacturas
        Dim doc = PdfDocument.FromFile(rutaFactura)
        doc.Sign(firma)
        doc.SaveAs(Path.Combine(dirSalida, Path.GetFileName(rutaFactura)))
    Next

    Return Results.Ok(New With {.FacturasFirmadas = rutasFacturas.Length})
End Function)
$vbLabelText   $csharpLabel

Para lotes muy grandes, las operaciones de firma de IronPDF son seguras para hilos: puede usar Parallel.ForEach si cada instancia de PdfDocument está aislada en su propio hilo.


Verificación programática de firmas digitales

Tras firmar y enviar un documento, es posible que necesite verificar que la firma sigue siendo válida:

app.MapGet("/facturas/verificar/{nombre}", (string nombre, IWebHostEnvironment env) =>
{
    string rutaFirmada = Path.Combine(env.ContentRootPath, "FacturasFirmadas", nombre);
    var documento = PdfDocument.FromFile(rutaFirmada);

    var firmas = documento.GetSignatures();

    foreach (var sig in firmas)
    {
        bool esValida = sig.VerifySignature();
        string estado = esValida
            ? $"Válida — firmada por {sig.SignerName} el {sig.SigningTime:dd/MM/yyyy}"
            : "INVÁLIDA — el documento puede haber sido manipulado";

        Console.WriteLine(estado);
    }

    return Results.Ok(new { NumeroFirmas = firmas.Count });
});
app.MapGet("/facturas/verificar/{nombre}", (string nombre, IWebHostEnvironment env) =>
{
    string rutaFirmada = Path.Combine(env.ContentRootPath, "FacturasFirmadas", nombre);
    var documento = PdfDocument.FromFile(rutaFirmada);

    var firmas = documento.GetSignatures();

    foreach (var sig in firmas)
    {
        bool esValida = sig.VerifySignature();
        string estado = esValida
            ? $"Válida — firmada por {sig.SignerName} el {sig.SigningTime:dd/MM/yyyy}"
            : "INVÁLIDA — el documento puede haber sido manipulado";

        Console.WriteLine(estado);
    }

    return Results.Ok(new { NumeroFirmas = firmas.Count });
});
Imports Microsoft.AspNetCore.Builder
Imports Microsoft.AspNetCore.Hosting
Imports System.IO

app.MapGet("/facturas/verificar/{nombre}", Function(nombre As String, env As IWebHostEnvironment)
    Dim rutaFirmada As String = Path.Combine(env.ContentRootPath, "FacturasFirmadas", nombre)
    Dim documento = PdfDocument.FromFile(rutaFirmada)

    Dim firmas = documento.GetSignatures()

    For Each sig In firmas
        Dim esValida As Boolean = sig.VerifySignature()
        Dim estado As String = If(esValida,
            $"Válida — firmada por {sig.SignerName} el {sig.SigningTime:dd/MM/yyyy}",
            "INVÁLIDA — el documento puede haber sido manipulado")

        Console.WriteLine(estado)
    Next

    Return Results.Ok(New With {.NumeroFirmas = firmas.Count})
End Function)
$vbLabelText   $csharpLabel

GetSignatures() devuelve todas las firmas digitales del PDF. Cada objeto PdfDigitalSignature expone VerifySignature(), el nombre del firmante, la marca de tiempo y la cadena de certificados — información suficiente para un registro de auditoría conforme a LOPDGDD.


Añadir imagen de firma visible (sello de empresa)

Para facturas y contratos que requieren representación visual de la firma además de la criptográfica — por ejemplo, el sello de la empresa —, IronPDF soporta la composición de la imagen en la página:

using IronPdf;
using IronPdf.Signing;
using IronSoftware.Drawing;

app.MapPost("/facturas/firmar-con-sello", (IWebHostEnvironment env) =>
{
    string pdfPath = Path.Combine(env.ContentRootPath, "Documents", "factura.pdf");
    var documento = PdfDocument.FromFile(pdfPath);

    string certPath = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx");
    string imagePath = Path.Combine(env.ContentRootPath, "Images", "sello_empresa.png");

    // Área donde aparece el sello visible (x, y, ancho, alto en puntos)
    var areaSello = new Rectangle(50, 680, 200, 80);

    var firma = new PdfSignature(certPath, "contraseñaCert");
    firma.LoadSignatureImageFromFile(imagePath, pageIndex: 0, areaSello);

    documento.Sign(firma);

    string rutaSalida = Path.Combine(Path.GetTempPath(), "factura_sellada.pdf");
    documento.SaveAs(rutaSalida);

    return Results.File(rutaSalida, "application/pdf", "factura_sellada.pdf");
});
using IronPdf;
using IronPdf.Signing;
using IronSoftware.Drawing;

app.MapPost("/facturas/firmar-con-sello", (IWebHostEnvironment env) =>
{
    string pdfPath = Path.Combine(env.ContentRootPath, "Documents", "factura.pdf");
    var documento = PdfDocument.FromFile(pdfPath);

    string certPath = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx");
    string imagePath = Path.Combine(env.ContentRootPath, "Images", "sello_empresa.png");

    // Área donde aparece el sello visible (x, y, ancho, alto en puntos)
    var areaSello = new Rectangle(50, 680, 200, 80);

    var firma = new PdfSignature(certPath, "contraseñaCert");
    firma.LoadSignatureImageFromFile(imagePath, pageIndex: 0, areaSello);

    documento.Sign(firma);

    string rutaSalida = Path.Combine(Path.GetTempPath(), "factura_sellada.pdf");
    documento.SaveAs(rutaSalida);

    return Results.File(rutaSalida, "application/pdf", "factura_sellada.pdf");
});
Imports IronPdf
Imports IronPdf.Signing
Imports IronSoftware.Drawing

app.MapPost("/facturas/firmar-con-sello", Function(env As IWebHostEnvironment)
    Dim pdfPath As String = Path.Combine(env.ContentRootPath, "Documents", "factura.pdf")
    Dim documento = PdfDocument.FromFile(pdfPath)

    Dim certPath As String = Path.Combine(env.ContentRootPath, "Certificates", "sello_empresa_fnmt.pfx")
    Dim imagePath As String = Path.Combine(env.ContentRootPath, "Images", "sello_empresa.png")

    ' Área donde aparece el sello visible (x, y, ancho, alto en puntos)
    Dim areaSello As New Rectangle(50, 680, 200, 80)

    Dim firma As New PdfSignature(certPath, "contraseñaCert")
    firma.LoadSignatureImageFromFile(imagePath, pageIndex:=0, areaSello)

    documento.Sign(firma)

    Dim rutaSalida As String = Path.Combine(Path.GetTempPath(), "factura_sellada.pdf")
    documento.SaveAs(rutaSalida)

    Return Results.File(rutaSalida, "application/pdf", "factura_sellada.pdf")
End Function)
$vbLabelText   $csharpLabel

La imagen visible se compone en la página especificada, y la firma criptográfica se aplica simultáneamente a todo el documento.


Campos de formulario de firma para firmantes externos

En flujos donde el documento lo genera su sistema pero debe ser firmado por un tercero (cliente, organismo regulador), puede integrar un campo de formulario de firma dedicado:

using IronPdf;
using IronPdf.Forms;

app.MapGet("/contratos/generar-firmable", (IWebHostEnvironment env) =>
{
    var renderer = new ChromePdfRenderer();
    var pdf = renderer.RenderHtmlAsPdf(@"
        <h1>Contrato de Prestación de Servicios</h1>
        <p>Revise los términos y firme en el campo indicado.</p>
    ");

    // Campo de firma con nombre, página, posición y dimensiones en puntos
    var campoFirma = new SignatureFormField(
        "FirmaCliente",
        pageIndex: 0,
        x: 50,
        y: 600,
        width: 300,
        height: 100
    );

    pdf.Form.Add(campoFirma);

    string rutaSalida = Path.Combine(Path.GetTempPath(), "contrato_para_firmar.pdf");
    pdf.SaveAs(rutaSalida);

    return Results.File(rutaSalida, "application/pdf", "contrato_para_firmar.pdf");
});
using IronPdf;
using IronPdf.Forms;

app.MapGet("/contratos/generar-firmable", (IWebHostEnvironment env) =>
{
    var renderer = new ChromePdfRenderer();
    var pdf = renderer.RenderHtmlAsPdf(@"
        <h1>Contrato de Prestación de Servicios</h1>
        <p>Revise los términos y firme en el campo indicado.</p>
    ");

    // Campo de firma con nombre, página, posición y dimensiones en puntos
    var campoFirma = new SignatureFormField(
        "FirmaCliente",
        pageIndex: 0,
        x: 50,
        y: 600,
        width: 300,
        height: 100
    );

    pdf.Form.Add(campoFirma);

    string rutaSalida = Path.Combine(Path.GetTempPath(), "contrato_para_firmar.pdf");
    pdf.SaveAs(rutaSalida);

    return Results.File(rutaSalida, "application/pdf", "contrato_para_firmar.pdf");
});
Imports IronPdf
Imports IronPdf.Forms

app.MapGet("/contratos/generar-firmable", Function(env As IWebHostEnvironment)
    Dim renderer = New ChromePdfRenderer()
    Dim pdf = renderer.RenderHtmlAsPdf("
        <h1>Contrato de Prestación de Servicios</h1>
        <p>Revise los términos y firme en el campo indicado.</p>
    ")

    ' Campo de firma con nombre, página, posición y dimensiones en puntos
    Dim campoFirma = New SignatureFormField(
        "FirmaCliente",
        pageIndex:=0,
        x:=50,
        y:=600,
        width:=300,
        height:=100
    )

    pdf.Form.Add(campoFirma)

    Dim rutaSalida As String = Path.Combine(Path.GetTempPath(), "contrato_para_firmar.pdf")
    pdf.SaveAs(rutaSalida)

    Return Results.File(rutaSalida, "application/pdf", "contrato_para_firmar.pdf")
End Function)
$vbLabelText   $csharpLabel

El destinatario puede abrir el PDF en Adobe Acrobat Reader, aplicar su propio certificado o firma electrónica, y devolver el documento firmado a su sistema para su validación y archivo.


Errores frecuentes al firmar PDFs en el ecosistema español

El certificado FNMT no aparece como de confianza en Adobe Acrobat Reader

Por defecto, Adobe Acrobat Reader solo confía en el almacén de certificados de Adobe (AATL). Los certificados FNMT no forman parte de esa lista. Para que la firma aparezca como válida sin advertencias, el receptor debe añadir el certificado raíz de la FNMT al almacén de confianza de su visor, o el emisor debe usar un certificado de una CA incluida en AATL. Para verificación programática mediante VerifySignature() en su propio sistema, esto no es un obstáculo.

Sign() se llama pero el PDF no está firmado al abrir el fichero

document.Sign(firma) prepara la firma pero no la persiste hasta que se llama a SaveAs(). El patrón correcto es siempre Sign()SaveAs().

La hacienda foral rechaza el XML TicketBAI pero el PDF está correcto

Son dos documentos independientes. El rechazo del XML por la hacienda foral (Bizkaia, Gipuzkoa o Araba) se debe a la firma XAdES o al formato del XML — no a la firma PAdES del PDF. Verifique los esquemas XAdES requeridos en la especificación técnica de cada hacienda foral antes de depurar en IronPDF.


Próximos pasos

La firma digital de PDF en ASP.NET Core para el ecosistema español requiere comprender qué estándar aplica en cada parte del flujo: PAdES para los PDFs de facturas (archivado LOPDGDD/eIDAS), XAdES para los XML TicketBAI (Bizkaia/Gipuzkoa/Araba), y certificados FNMT como la autoridad de confianza reconocida por la AEAT y las haciendas forales.

IronPDF gestiona la capa PAdES para que pueda concentrarse en la lógica de negocio de facturación. Para continuar:

Inicie con una prueba gratuita de IronPDF y tenga su primer PDF firmado para el ecosistema español en funcionamiento en menos de una hora. Si tiene preguntas sobre TicketBAI, VeriFactu o LOPDGDD, el equipo de soporte de Iron Software está disponible para ayudarle.

Preguntas Frecuentes

¿Qué es una firma digital en ASP.NET Core?

Una firma digital en ASP.NET Core es como un sello de lacre digital que se utiliza para verificar la autenticidad y la integridad de los documentos PDF. Garantiza que los documentos son legalmente válidos y no han sido manipulados.

¿Cómo puedo añadir una firma digital a un documento PDF con IronPDF?

Puede añadir una firma digital a un documento PDF mediante IronPDF incluyendo un certificado y configurándolo para que firme el documento, garantizando que sea seguro y verificable.

¿Por qué son importantes las firmas digitales para mis documentos empresariales?

Las firmas digitales son cruciales porque verifican que documentos como contratos o facturas son auténticos y no han sido modificados, protegiendo así a su empresa de posibles riesgos legales.

¿Puedo crear campos de formulario interactivos en PDF con IronPDF?

Sí, IronPDF permite crear campos de formulario interactivos en PDF, lo que puede mejorar la interacción del usuario y agilizar los procesos de documentos en aplicaciones ASP.NET Core.

¿Es posible añadir firmas visibles a mis documentos PDF?

Sí, con IronPDF puede añadir firmas visibles a sus documentos PDF, dejando claro a los destinatarios que el documento está firmado y verificado de forma segura.

¿Qué tipos de certificados pueden utilizarse para las firmas digitales en PDF?

Puede utilizar varios tipos de certificados para las firmas digitales en PDF, incluidos los certificados autofirmados y los emitidos por una autoridad de certificación de confianza, en función del nivel de seguridad y confianza requerido.

¿Cómo garantiza IronPDF que un documento PDF no ha sido manipulado?

IronPDF garantiza que un documento PDF no ha sido manipulado mediante el uso de firmas digitales que validan la integridad y autenticidad del documento, alertando a los destinatarios si se han realizado cambios después de la firma.

¿Puedo automatizar el proceso de firma digital en aplicaciones ASP.NET Core?

Sí, puede automatizar el proceso de firma digital en aplicaciones ASP.NET Core mediante IronPDF, que permite el procesamiento por lotes y la integración en flujos de trabajo existentes.

Curtis Chau
Escritor Técnico

Curtis Chau tiene una licenciatura en Ciencias de la Computación (Carleton University) y se especializa en el desarrollo front-end con experiencia en Node.js, TypeScript, JavaScript y React. Apasionado por crear interfaces de usuario intuitivas y estéticamente agradables, disfruta trabajando con frameworks modernos y creando manuales bien ...

Leer más

Equipo de soporte de Iron

Estamos disponibles online las 24 horas, 5 días a la semana.
Chat
Email
Llámame