Signum.Pdf.NetFramework 1.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Signum.Pdf.NetFramework --version 1.1.0
                    
NuGet\Install-Package Signum.Pdf.NetFramework -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Signum.Pdf.NetFramework" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Signum.Pdf.NetFramework" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Signum.Pdf.NetFramework" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Signum.Pdf.NetFramework --version 1.1.0
                    
#r "nuget: Signum.Pdf.NetFramework, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Signum.Pdf.NetFramework@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Signum.Pdf.NetFramework&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Signum.Pdf.NetFramework&version=1.1.0
                    
Install as a Cake Tool

Signum.Pdf

Firma digital PAdES para .NET, sin ataduras. Firma PDFs en PAdES-B-LTA —firma + sello de tiempo + validación a largo plazo— usando solo dependencias MIT. PAdES es un estándar ETSI de alcance internacional, así que la librería sirve para cualquier PKI; está probada contra la infraestructura de firma digital de Costa Rica (tarjetas SINPE/BCCR, TSA de SINPE).

Validado contra el validador nacional de Costa Rica. Un documento firmado con Signum pasa los cuatro checks en verde: integridad y autenticidad, validez en el tiempo, revocación contenida en el documento y fecha oficial de la firma sin advertencias. Probado en producción con tarjetas SINPE y la TSA del BCCR.

Límite conocido: las tablas de referencias cruzadas comprimidas (xref streams) se leen, pero las revisiones que agrega Signum se escriben siempre con tabla clásica.

  • Los cuatro checks del validador nacional en verde
  • Firma diferida (deferred signing): el PDF nunca sale del servidor; la tarjeta firma solo un hash de 32 bytes
  • Firma directa con certificado del almacén/PFX (sellos institucionales)
  • PAdES-B-LTA completo: CAdES + sello de tiempo de la firma (CAdES-T) + DSS con OCSP/CRL embebidos + Document Timestamp
  • Estampa visible configurable estilo Adobe (o firma invisible)
  • Lotes con id/guid propio y aislamiento de errores por documento
  • Lectura de firmas existentes: ¿quién firmó? ¿ya firmó este usuario?
  • Licencia MIT — sin AGPL, sin regalías, sin sorpresas
  • Dependencias: PDFsharp (MIT) + BouncyCastle.Cryptography (MIT)

Instalación

dotnet add package Signum.Pdf

Requiere .NET 8, 9 o 10.

Activación y registro

Signum se usa por inyección de dependencias y requiere una clave de activación:

// Program.cs
builder.Services.AddSignum(builder.Configuration["Signum:ClaveActivacion"]!, o =>
{
    o.TsaUrl = "http://tsa.sinpe.fi.cr/tsahttp/";              // autoridad de sellado por defecto
    o.Traza  = m => logger.LogInformation("Signum: {M}", m);   // diagnóstico opcional
});
// Uso: se inyecta ISignum — la única superficie pública del paquete
public sealed class MiServicio(ISignum signum) { /* … */ }

Sin una clave válida, AddSignum lanza SignumActivacionException en el arranque y no registra nada. Si la firma es una función opcional de tu app, usá TryAddSignum, que devuelve false en vez de lanzar.

Para solicitar una clave de activación, escribí a andreyclemen@gmail.com.


Inicio rápido

1. Firma diferida (tarjeta inteligente) — el caso típico

El flujo es en dos fases porque la llave privada vive en la tarjeta del usuario (en otra máquina) y solo puede firmar un hash:

// ───── FASE 1 (servidor): preparar ─────
var prep = signum.Firmar(pdfBytes)
    .ConCadena(cadenaDer)          // la CADENA de certificados de la tarjeta (ver nota abajo)
    .ConEstampa(e => e
        .EnPagina(1)
        .Recuadro(x: 90, y: 350, ancho: 300, alto: 58))  // puntos PDF, origen abajo-izquierda
        // el nombre NO se pasa: sale solo del certificado (CN). Look default = nombre grande |
        // "Firmado digitalmente por … / Fecha: …"
    .Preparar();

// prep.Digest -> LO QUE FIRMA LA TARJETA: la huella (32 bytes) del documento + sus atributos.
//                Es lo ÚNICO que viaja al agente/tarjeta; el PDF nunca sale del servidor.
// prep.Estado -> LA MEMORIA DE LA OPERACIÓN: el PDF preparado + los datos exactos que Signum
//                necesita para terminar. Guardalo donde quieras (memoria, BD, Redis) mientras
//                el usuario digita el PIN, y devolvéselo a Completar en la fase 2.

// ───── (el agente local le pide el PIN a la tarjeta y firma prep.Digest) ─────

// ───── FASE 2 (servidor): completar ─────
byte[] pdfFirmado = signum.Completar(prep.Estado, firmaDeLaTarjeta)
    .ConSelloDeTiempo("http://tsa.sinpe.fi.cr/tsahttp/")   // CAdES-T: la "fecha oficial"
    .ConLtv("http://tsa.sinpe.fi.cr/tsahttp/")             // DSS + DocTimeStamp = B-LTA
    .Emitir();

¿Qué es ConCadena(cadenaDer)? En la firma diferida el certificado NO está en el servidor: vive en la tarjeta del usuario. El agente local lo lee (parte pública, sin PIN) y manda la cadena completa: certificado del firmante + CAs intermedias + raíz, cada uno en DER (byte[]). Signum la necesita para: (1) meter los certificados dentro de la firma (el check "jerarquía de confianza"), (2) sacar el nombre/cédula del firmante, y (3) el LTV (a quién pedirle OCSP/CRL). En la firma directa no existe: ConCertificado(x509) ya trae todo.

2. Firma directa (sello institucional, PFX/almacén)

byte[] sellado = signum.Firmar(pdfBytes)
    .ConCertificado(certificadoConLlave)    // X509Certificate2 (almacén de Windows o PFX)
    .ConEstampa(e => e
        .EnPagina(1).Recuadro(246, 76, 300, 48)
        .SinGrafico()
        .Firmante("TU INSTITUCION (SELLO ELECTRONICO)")
        .MostrarMotivo("Sello electrónico institucional"))
    .ConSelloDeTiempo(tsaUrl)
    .ConLtv(tsaUrl)
    .Emitir();

3. Firma invisible

No llames ConEstampa — la firma queda en el PDF sin marca visual:

var prep = signum.Firmar(pdfBytes).ConCadena(cadenaDer).Preparar();

La estampa visible

Se configura igual que el diálogo "Configurar aspecto de la firma" de Adobe Acrobat:

.ConEstampa(e => e
    .EnPagina(1)
    .Recuadro(90, 350, 300, 58)
    .ConLogotipo(pngLogoDeTuEmpresa, opacidad: 0.12f))   // marca de agua tenue (opcional)

// Con SOLO eso, la estampa sale con el look por defecto (dos columnas estilo Adobe):
//
//   JOSE ANDREY SALAZAR   ⟨logo⟩   Firmado digitalmente por JOSE
//   GUZMAN (FIRMA)         tenue   ANDREY SALAZAR GUZMAN (FIRMA)
//                                  Fecha: 2026.07.27 11:20:13 -06'00'
//
// El nombre sale del CERTIFICADO (CN) — no hay que pasarlo. Texto auto-ajustado al recuadro.

// ── Opciones (todas opcionales, estilo diálogo de Adobe) ──
//  GRÁFICO (columna izquierda) — elegí UNO:
//    .GraficoNombre()                       // default: el nombre en grande
//    .GraficoImagen(pngFirmaManuscrita)     // tu firma escaneada (PNG con transparencia)
//    .SinGrafico()                          // solo el bloque de texto
//  TEXTO (columna derecha):
//    .Firmante("OTRO NOMBRE")               // override (default: CN del certificado)
//    .MostrarFecha()                        // ON por default (reloj del servidor; informativa)
//    .MostrarUbicacion("San José, Costa Rica")
//    .MostrarMotivo("Aprobación del documento")
//    .MostrarNombreReconocimiento()         // el DN completo del certificado
//    .MostrarVersion()                      // "Signum x.y"
//    .ConEtiquetas(false)                   // quita los prefijos "Fecha:", "Motivo:"…

Notas:

  • El texto se auto-ajusta al recuadro: dibujes el rectángulo grande o chico, el nombre y la fecha SIEMPRE salen completos.
  • MostrarFecha usa el reloj del servidor al preparar — es informativa, igual que en Adobe. La fecha oficial (la que reporta el validador) es la del sello de tiempo del TSA (ConSelloDeTiempo), que es criptográfica y no depende de ningún reloj local.
  • Signum no trae ningún logo embebido: el logotipo y la imagen de firma los pasás vos (PNG).

Leer firmas existentes — "¿ya firmó?"

// Listar todo
foreach (var f in signum.LeerFirmas(pdfBytes))
    Console.WriteLine($"{f.Firmante} ({f.Cedula}) — oficial: {f.FechaOficial} — íntegra: {f.Integra}");

// ¿Ya firmó este usuario? — tolerante al formato de la cédula (todas equivalen):
signum.YaFirmo(pdf, "CPF-05-0398-0891");
signum.YaFirmo(pdf, "05-0398-0891");
signum.YaFirmo(pdf, "0503980891");
signum.YaFirmo(pdf, "503980891");

// Por nombre, estilo LIKE '%…%' (sin importar mayúsculas ni tildes):
signum.YaFirmoPorNombre(pdf, "salazar guzmán");

// Búsqueda que devuelve LAS firmas que matchean:
var mias = signum.BuscarFirmas(pdf, cedula: "503980891");
  • La comparación es contra el certificado (CN y SERIALNUMBER del subject), nunca contra el texto dibujado de la estampa.
  • Integra verifica la criptografía (firma RSA + digest del documento). La confianza de cadena y la revocación las evalúa el validador — están fuera del alcance de esta API.
  • Los sellos de tiempo de documento (EsDocTimeStamp == true) no cuentan como firmantes.

Ejemplo: bloquear el doble firmado

if (signum.YaFirmo(pdfBytes, cedulaDelUsuario))
{
    ToastWarning("Ya firmaste este documento.");
    return;
}
// ... abrir el flujo de firma

LTV y niveles PAdES

Llamada Nivel Qué agrega Validador CR
solo Emitir() B-B firma CAdES integridad ✅, fecha ❌, validez en el tiempo ❌
+ ConSelloDeTiempo(tsa) B-T sello de tiempo DE LA FIRMA (CAdES-T) + fecha oficial ✅
+ ConLtv(tsa) B-LTA DSS (cadena + OCSP/CRL embebidos) + Document Timestamp + DSS del TSA todo verde

ConLtv también sirve standalone sobre un PDF ya firmado:

byte[] conLtv = signum.AgregarLtv(pdfYaFirmado).ConTsa(tsaUrl).Emitir();

Comportamiento ante fallos de red (importante)

El LTV nunca pierde tu firma:

  • Si la TSA no responde al agregar el Document Timestamp → se lanza SignumLtvTimestampException, que trae el PDF con el DSS ya aplicado (PdfConDss) para que decidas usarlo.
  • Si falla la recolección de revocación completa → recibís la firma sin LTV (B-T), con el error disponible.
  • Los fetch de OCSP/CRL corren en paralelo con timeouts cortos (OCSP 3s, CRL 10s, TSA 5s) y caché por certificado con expiración según el nextUpdate real — en lotes, del segundo documento en adelante el LTV no toca la red (~90 ms).

Rendimiento

Números de referencia (medidos contra la TSA de SINPE y las CAs de CR):

Operación Frío Con caché
Completar entero (CMS + CAdES-T + LTV) ~320 ms ~150 ms
LTV solo (DSS + DocTimeStamp + DSS del sello) ~150 ms ~75 ms
Lote de 3 documentos, completo ~430 ms

Consejo: llamá signum.PrecalentarRevocacion(cadenaDer) (fire-and-forget) apenas conozcás la cadena del firmante — p. ej. mientras el usuario digita el PIN — y el LTV encontrará todo en caché.


Firmar varios PDFs (lote)

API de lote de primera clase: pasás un arreglo con tu id (string o guid), el PDF y la estampa opcional de cada uno — y recibís el mismo arreglo con el PDF firmado, correlacionado por id. Aislamiento por documento (uno que falla no tumba el resto) y cachés compartidos: el primer documento paga la red; del segundo en adelante el LTV tarda ~90 ms.

var docs = new[]
{
    new DocumentoParaFirmar
    {
        Id = idSolicitud1.ToString(),                 // tu id o guid — vuelve tal cual
        Pdf = pdf1,
        Estampa = e => e.EnPagina(1).Recuadro(90, 350, 300, 58),
    },
    new DocumentoParaFirmar { Id = "doc-2", Pdf = pdf2 },   // sin Estampa -> firma invisible
    new DocumentoParaFirmar { Id = "doc-3", Pdf = pdf3, Firmante = "NOMBRE OVERRIDE" },
};

Lote con tarjeta (diferida, 2 fases)

// FASE 1: preparar TODO el lote (prefetch de revocación automático)
var lote = signum.PrepararLote(docs, cadenaDer);
// lote.Pendientes -> [(Id, Digest)]  — la tarjeta firma cada Digest
// lote.Estado     -> byte[] serializable del lote completo

// (el agente firma los digests; hoy pide PIN por firma — ver nota)

// FASE 2: completar TODO, correlacionado por Id
var resultados = signum.CompletarLote(lote.Estado, firmas, tsaUrl);   // firmas: [(Id, FirmaPkcs1)]

foreach (var r in resultados)
    if (r.Exito) Guardar(r.Id, r.Pdf);
    else         Registrar(r.Id, r.Error);    // este doc falló; los demás siguieron

Lote directo (sello institucional, 1 llamada)

var resultados = signum.FirmarLote(docs, certificadoConLlave, tsaUrl);
// mismo retorno: [(Id, Pdf firmado | Error)]

Sobre el PIN (diferida): la tarjeta puede firmar varios digests con una sola sesión de PIN, pero si tu agente de firma pide el PIN una vez por documento —una decisión de seguridad razonable— el lote se lo pedirá N veces. Eso se decide en el agente, no acá: Signum ya recibe las N firmas juntas en CompletarLote.


Fuentes

No hay nada que configurar. Signum trae embebida Liberation Sans (SIL OFL 1.1), métricamente compatible con Helvetica y Arial —las tipografías de siempre en los visores de PDF—, y la registra sola la primera vez que dibuja una estampa. El resultado no depende de las fuentes que tenga el servidor, lo que importa porque un contenedor mínimo no trae ninguna.

Si tu aplicación ya registró un IFontResolver de PDFsharp (por ejemplo para usar tipografías corporativas), Signum respeta el tuyo y no lo sobrescribe.

Alcance y límites

  • Entrada: PDFs sin cifrar (PDF 1.4–2.0, xref clásico o comprimido). PDFs cifrados: no soportados.
  • Multi-firma: se puede firmar un documento que ya está firmado — pueden firmar todas las personas que haga falta. La firma nueva se agrega como revisión incremental (los bytes ya firmados quedan intactos), y el LTV re-emite el /DSS con el material de TODAS las firmas.
  • El digest es SHA-256 y la firma RSA PKCS#1 v1.5 (lo que producen las tarjetas SINPE). Otros algoritmos: fuera del alcance inicial.
  • Verificación (LeerFirmas): integridad criptográfica, no evaluación de confianza/revocación.

Conformidad: qué estándar cumple, y en qué países sirve

PAdES no es un formato costarricense. Es un estándar de ETSI, el organismo europeo de normalización, publicado como EN 319 142. Costa Rica no definió un formato propio: lo adoptó. La Política de Formatos Oficiales de los Documentos Electrónicos Firmados Digitalmente (MICITT/DCFD, publicada en La Gaceta N.º 95 del 20 de mayo de 2013; vigente la v4 de junio de 2025) establece PAdES para los PDF, exige LTV —validación a largo plazo— y recomienda el perfil Baseline. Eso es exactamente lo que produce ConSelloDeTiempo + ConLtv: PAdES Baseline B-LTA, el nivel más alto del perfil.

Como el estándar es internacional, el mismo PDF que Signum firma para Costa Rica vale en cualquier país que reconozca PAdES —toda la Unión Europea por eIDAS, y buena parte de Latinoamérica—. Lo que cambia de un país a otro es configuración, no código:

Qué cambia Cómo se resuelve
Autoridad de sellado de tiempo Parámetro: ConSelloDeTiempo("https://tsa-de-tu-pais/…"). Sirve cualquier TSA RFC 3161
Cadena de certificados La aporta la tarjeta o el PFX, sea del país que sea
A quién pedirle OCSP/CRL Automático: sale de las extensiones AIA y CRL Distribution Points del propio certificado
Cómo se ve la estampa Configurable, y sin ningún logo embebido

Dos límites reales, que conviene conocer antes de adoptarla fuera de Costa Rica:

  1. Solo RSA PKCS#1 v1.5 sobre SHA-256, que es lo que emiten las tarjetas costarricenses. Una PKI que firme con ECDSA o RSA-PSS todavía no está soportada.
  2. YaFirmo asume una identificación numérica. Normaliza a dígitos para tolerar los formatos de la cédula de Costa Rica (CPF-05-0398-0891503980891), así que con un documento alfanumérico —un NIE español, por ejemplo— descarta las letras. YaFirmoPorNombre y BuscarFirmas(nombre:) no tienen esa limitación.

Todo lo demás —el CMS, el sello de tiempo, el DSS, la revisión incremental— es agnóstico del país.

Estándares implementados

ISO 32000-2 (PDF 2.0, §12.8 firmas, §7.5.6 incremental updates) · ETSI EN 319 142-1 (PAdES baseline B/T/LT/LTA) · ETSI TS 102 778-4 (LTV: DSS y Document Timestamp) · RFC 5652 (CMS) · RFC 5035 (ESS signingCertificateV2) · RFC 3161 (sello de tiempo) · RFC 6960 (OCSP).

Cada clase cita en su documentación la sección de la especificación que implementa. Signum es una implementación limpia desde los estándares públicos; no deriva de ninguna librería con licencia restrictiva.

Licencia

MIT.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Ver CHANGELOG.md