通过 C# 为 PDF 文档添加数字签名
Source: Dev.to
数字签名是实现最新文档合规性和数据安全的必备手段。
对于企业级 .NET 开发而言,使用 C# 实现 PDF 数字签名功能是合同管理、官方文件发布以及机密文件分发等场景中的高优先级需求。
Free Spire.PDF for .NET 是一个轻量级、免费的 PDF 处理库,可实现无缝的 PDF 数字签名集成——无需使用 Adobe Acrobat 等第三方软件。
1️⃣ 安装
Install-Package FreeSpire.PDF
NuGet 命令会自动解析依赖并将库集成到您的 .NET 项目中。
2️⃣ 证书要求
| 环境 | 获取 .pfx 证书的方法 |
|---|---|
| 测试 | 使用 OpenSSL 或 Windows 证书管理器 生成自签名证书(仅适用于功能测试)。 |
| 生产 | 使用受信任的 证书颁发机构 (CA) 签发的证书,且符合当地法规。 |
证书必须同时包含公钥和私钥。
加载证书
核心类是 System.Security.Cryptography.X509Certificates.X509Certificate2。
3️⃣ 签名工作流
PdfOrdinarySignatureMaker(Free Spire.PDF)处理标准数字签名。
线性工作流如下:
- 加载 PDF 文档
- 解析 X.509 证书
- 初始化签名生成器
- 配置签名设置
- 执行签名
- 保存并释放资源
我们将首先实现一个 基本的不可见签名,随后扩展为带有文本和图像元素的 可自定义可见签名。
4️⃣ 基础隐形签名(生产级)
using Spire.Pdf;
using Spire.Pdf.Interactive.DigitalSignatures;
using System.Security.Cryptography.X509Certificates;
using System.IO;
namespace DigitallySignPdf
{
class Program
{
static void Main(string[] args)
{
// Define file paths (use relative paths for better portability)
string inputPdfPath = "Input.pdf";
string outputPdfPath = "Signed_Invisible.pdf";
string certPath = "certificate.pfx";
string certPassword = "abc123";
try
{
// Validate input files exist to avoid runtime exceptions
if (!File.Exists(inputPdfPath) || !File.Exists(certPath))
{
Console.WriteLine("Error: Input PDF or certificate file not found.");
return;
}
// 1. Initialize PdfDocument and load the target PDF
using (PdfDocument doc = new PdfDocument())
{
doc.LoadFromFile(inputPdfPath);
// 2. Load X.509 certificate with secure key storage flags
// Key flags explanation:
// - MachineKeySet: Suitable for server‑side deployment (shared key store)
// - EphemeralKeySet: Prevents key persistence, enhances security for temporary signing tasks
X509Certificate2 cert = new X509Certificate2(
certPath,
certPassword,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet
);
// 3. Initialize signature maker and bind the PDF document
PdfOrdinarySignatureMaker signatureMaker = new PdfOrdinarySignatureMaker(doc, cert);
// 4. Execute signature with a unique identifier (for later verification)
signatureMaker.MakeSignature("Enterprise_Signature_001");
// 5. Save the signed PDF (using 'using' statement auto‑releases resources)
doc.SaveToFile(outputPdfPath);
Console.WriteLine($"Success! Invisible signature added. Output file: {outputPdfPath}");
}
}
catch (CryptographicException ex)
{
Console.WriteLine($"Certificate error: {ex.Message} (Check password or certificate validity)");
}
catch (IOException ex)
{
Console.WriteLine($"File operation error: {ex.Message} (Ensure the PDF is not open in another program)");
}
catch (Exception ex)
{
Console.WriteLine($"Signature failed: {ex.Message}");
}
}
}
}
注意: 隐形签名对终端用户没有可视化确认。
5️⃣ 高级可见签名(可自定义)
下面的示例在 PDF 中添加一个可自定义文本、图像和布局的可见签名字段——非常适合合同和正式文件。
using Spire.Pdf;
using Spire.Pdf.Interactive.DigitalSignatures;
using System;
using System.Drawing;
using System.Security.Cryptography.X509Certificates;
using System.IO;
namespace PdfDigitalSignatureDemo
{
class AdvancedVisibleSignatureDemo
{
static void Main(string[] args)
{
string inputPdf = "Input.pdf";
string outputPdf = "Signed_Visible.pdf";
string certPath = "certificate.pfx";
string certPwd = "your_secure_password";
string signatureImagePath = "signature_stamp.png";
try
{
using (PdfDocument doc = new PdfDocument())
{
doc.LoadFromFile(inputPdf);
X509Certificate2
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
}
代码片段故意保留不完整,以保持原始内容。
6️⃣ 回顾与技巧
| Topic | Tips |
|---|---|
| Certificate handling | 在服务器环境中首选 MachineKeySet + EphemeralKeySet;在生产环境中绝不要硬编码密码。 |
| Error handling | 捕获 CryptographicException、IOException 和通用 Exception,以提供清晰的诊断信息。 |
| File paths | 使用相对路径或配置设置,以提升在不同环境间的可移植性。 |
| Visible signatures | 调整签名矩形区域,添加文本(PdfSignatureAppearance),并嵌入图像,以获得专业外观。 |
| Testing | 使用 Adobe Acrobat Reader 或任何支持数字签名的 PDF 阅读器验证签名。 |
7️⃣ 后续步骤
- 完成可见签名演示 – 添加矩形定位、外观设置和图像加载。
- 将其集成到 CI/CD 流水线 – 在分发前自动对生成的 PDF 进行签名。
- 实现验证逻辑 – 使用
PdfSignatureValidator(或第三方验证器)在接收端确认签名完整性。
祝签名愉快! 🚀
代码示例 – 添加可见的 PDF 签名
try
{
// Load the certificate (use secure storage flags)
var cert = new X509Certificate2(
certPath,
certPwd,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet);
// Initialise the signer and obtain the signature object
PdfOrdinarySignatureMaker signer = new PdfOrdinarySignatureMaker(doc, cert);
PdfSignature signature = signer.Signature;
// Configure signature metadata (visible in PDF signature properties)
signature.Name = "ACME Corporation Document Signing";
signature.ContactInfo = "compliance@acme.com";
signature.Location = "USA";
signature.Reason = "Confirm document content compliance and authenticity";
signature.Date = DateTime.Now; // Auto‑set signature timestamp
// Create and customise the signature appearance
PdfSignatureAppearance appearance = new PdfSignatureAppearance(signature)
{
NameLabel = "Signer: ",
ContactInfoLabel = "Contact: ",
LocationLabel = "Location: ",
ReasonLabel = "Reason: "
};
// Add a signature image (PNG/JPG). Resize if needed.
if (File.Exists(signatureImagePath))
{
PdfImage signatureImage = PdfImage.FromFile(signatureImagePath);
appearance.SignatureImage = signatureImage;
// Layout mode: Image + Text details
// Alternatives: SignImageOnly / SignDetailOnly
appearance.GraphicMode = GraphicMode.SignImageAndSignDetail;
}
else
{
Console.WriteLine("Signature image not found; using text‑only appearance.");
appearance.GraphicMode = GraphicMode.SignDetailOnly;
}
// Attach the appearance to the signer
signer.SignatureAppearance = appearance;
// Choose the target page (e.g., the last page of the PDF)
PdfPageBase targetPage = doc.Pages[doc.Pages.Count - 1];
// Set signature position (x, y, width, height) – adjust for your layout
const float x = 54f; // Left margin (72 pt = 1 in)
const float y = 330f; // Bottom margin
const float width = 280f;
const float height = 90f;
// Execute the visible signature
signer.MakeSignature(
"ACME_Visible_Signature_001",
targetPage,
x, y, width, height,
appearance);
// Save the signed PDF
doc.SaveToFile(outputPdf);
Console.WriteLine($"Visible signature added successfully! Output: {outputPdf}");
}
catch (Exception ex)
{
Console.WriteLine($"Advanced signature failed: {ex.Message}");
}
核心组件词汇表与实用技巧
| 类 / 参数 | 核心功能 | 使用说明 |
|---|---|---|
| PdfOrdinarySignatureMaker | 核心签名引擎;将 PDF 与证书绑定后执行签名 | 使用 using 语句或调用 Dispose() 及时释放资源。 |
| X509Certificate2 | 解析 .pfx 证书;提取公钥/私钥对 | 始终提供安全的密钥存储标志;在服务器上避免使用 DefaultKeySet。 |
| X509KeyStorageFlags | 控制证书的存放位置以及密钥的生命周期 | • MachineKeySet – 用于服务器端部署 • EphemeralKeySet – 临时、仅在内存中的密钥 |
| PdfSignatureAppearance | 配置可见签名的样式(标签、图像、布局) | 调整 GraphicMode 可在仅图像、仅文字或图文混合布局之间切换。 |
| GraphicMode | 定义可见签名的布局 | 选项:SignImageOnly、SignDetailOnly、SignImageAndSignDetail |
| MakeSignature() | 触发签名过程 | 传入 唯一 的签名名称,以便后续验证。 |
安全最佳实践
- 绝不要硬编码密码。 将证书密码存放在安全保管库(如 Azure Key Vault、AWS Secrets Manager)或加密的配置文件中。
- 限制证书访问权限。 只授予 .pfx 文件所需的最小文件系统权限。
- 应用池身份。 在 IIS 中,给应用池身份(例如
IIS AppPool\YourApp)授予对证书存储的读取权限。 - 在服务器上避免使用
UserKeySet。 该设置会在非交互式环境中导致权限问题。
常见错误场景与解决方案
| 错误类型 | 根本原因 | 解决方案 |
|---|---|---|
| CryptographicException | 证书密码无效或权限不足 | 验证密码,仔细检查 X509KeyStorageFlags,并确保应用池能够读取证书。 |
| IOException | PDF 文件被其他进程锁定 | 在签名之前关闭所有打开 PDF 的阅读器/编辑器。 |
| Invisible Signature Not Detected | 签名名称冲突或 PDF 保存不完整 | 每次使用唯一的签名名称,并确认在释放前已调用 SaveToFile()。 |
Validation Checklist
- Desktop readers: Adobe Acrobat DC, Foxit Reader
- Web browsers: Chrome PDF Viewer, Microsoft Edge PDF Reader
- Mobile: Adobe Acrobat Mobile, WPS Mobile
在上述环境中测试已签名的 PDF,确保签名被识别并正确显示。
Why Choose Spire.PDF for .NET?
- Lightweight & easy‑to‑integrate API – no heavyweight third‑party dependencies. → 轻量且易于集成的 API – 无笨重的第三方依赖。
- Full support for invisible and visible signatures via
PdfOrdinarySignatureMaker. → 通过PdfOrdinarySignatureMaker完全支持不可见和可见签名。 - Comprehensive documentation & examples that address common enterprise pain points (security, permissions, deployment). → 全面的文档和示例,解决常见企业痛点(安全、权限、部署)。
无论您是在构建文档管理系统、合同签署平台,还是机密文件分发工具,这种方法都能提供 成本效益高、高性能、可投入生产 的 PDF 数字签名解决方案。