Add PDF signing feature with UI and backend support
Introduced a new `PdfSigningService` for digitally signing PDFs using a PFX certificate and DevExpress's `PdfDocumentSigner`. Updated `Program.cs` to register the service and added a new configuration section `PdfSigning` in `appsettings.json` for certificate management. Added a Razor Page `TestSignature.cshtml` and its page model `TestSignature.cshtml.cs` to provide a user interface for testing PDF signing. The page includes file upload, signature validation, and result display functionality. Implemented supporting classes `PdfSigningOptions` for signature customization and `SignatureInformation` for extracting and displaying signature details, including signer name, location, and certificate validity.
This commit is contained in:
90
DXApp.TemplateKitProject/Pages/TestSignature.cshtml
Normal file
90
DXApp.TemplateKitProject/Pages/TestSignature.cshtml
Normal file
@@ -0,0 +1,90 @@
|
||||
@page
|
||||
@model DXApp.TemplateKitProject.Pages.TestSignatureModel
|
||||
@{
|
||||
ViewData["Title"] = "PDF-Signatur testen";
|
||||
}
|
||||
|
||||
<div class="content-block">
|
||||
<h2><i class="dx-icon-key"></i> PDF-Signatur Test</h2>
|
||||
<p class="text-muted">Einfacher Test zum Signieren einer PDF mit dem DevExpress PdfDocumentSigner.</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="mt-4">
|
||||
<div class="mb-3">
|
||||
<label for="pdfFile" class="form-label">PDF-Datei auswählen</label>
|
||||
<input type="file" class="form-control" id="pdfFile" name="PdfFile" accept=".pdf" required />
|
||||
<span asp-validation-for="PdfFile" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="dx-icon-key"></i> PDF signieren
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@if (Model.Success)
|
||||
{
|
||||
<div class="alert alert-success mt-4">
|
||||
<h5><i class="dx-icon-check"></i> ? PDF erfolgreich signiert!</h5>
|
||||
<p class="mb-2">
|
||||
<strong>Original:</strong> @Model.OriginalFileName (@Model.OriginalSizeKb KB)<br />
|
||||
<strong>Signiert:</strong> @Model.SignedFileName (@Model.SignedSizeKb KB)<br />
|
||||
<strong>Gespeichert:</strong> <code>@Model.OutputPath</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (Model.SignatureInfo != null && Model.SignatureInfo.Count > 0)
|
||||
{
|
||||
<div class="alert alert-info mt-3">
|
||||
<h6><i class="dx-icon-info"></i> Signatur-Details:</h6>
|
||||
@foreach (var sig in Model.SignatureInfo)
|
||||
{
|
||||
<div class="mb-3 p-2 border-start border-3 border-primary">
|
||||
<strong>Signatur:</strong> @sig.FieldName<br />
|
||||
<strong>Unterzeichner:</strong> @sig.SignerName<br />
|
||||
<strong>Ort:</strong> @sig.Location<br />
|
||||
<strong>Grund:</strong> @sig.Reason<br />
|
||||
<strong>Datum:</strong> @sig.Date.ToString("dd.MM.yyyy HH:mm:ss")<br />
|
||||
<strong>Signatur gültig:</strong>
|
||||
@if (sig.IsSignatureValid)
|
||||
{
|
||||
<span class="badge bg-success">? Ja</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger">? Nein</span>
|
||||
}
|
||||
<br />
|
||||
<strong>Zertifikat gültig:</strong>
|
||||
@if (sig.IsCertificateValid)
|
||||
{
|
||||
<span class="badge bg-success">? Ja</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger">? Nein</span>
|
||||
}
|
||||
<br />
|
||||
<strong>Zertifikat gültig bis:</strong> @sig.CertificateValidUntil.ToString("dd.MM.yyyy")<br />
|
||||
<small class="text-muted">Issuer: @sig.CertificateIssuer</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger mt-4">
|
||||
<i class="dx-icon-warning"></i> <strong>Fehler:</strong> @Model.ErrorMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="alert alert-light mt-4">
|
||||
<h6>?? Test-Schritte:</h6>
|
||||
<ol class="mb-0">
|
||||
<li>PDF-Datei auswählen (z.B. eine ZUGFeRD-Rechnung aus <code>/Invoices</code>)</li>
|
||||
<li>Auf "PDF signieren" klicken</li>
|
||||
<li>Signierte PDF wird in <code>C:\PdfResults</code> gespeichert</li>
|
||||
<li>Mit Adobe Acrobat Reader öffnen und Signatur prüfen</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
82
DXApp.TemplateKitProject/Pages/TestSignature.cshtml.cs
Normal file
82
DXApp.TemplateKitProject/Pages/TestSignature.cshtml.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using DXApp.TemplateKitProject.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace DXApp.TemplateKitProject.Pages;
|
||||
|
||||
public class TestSignatureModel(
|
||||
PdfSigningService signingService,
|
||||
IConfiguration configuration,
|
||||
ILogger<TestSignatureModel> logger) : PageModel
|
||||
{
|
||||
[BindProperty]
|
||||
public IFormFile? PdfFile { get; set; }
|
||||
|
||||
public bool Success { get; private set; }
|
||||
public string? ErrorMessage { get; private set; }
|
||||
public string? OriginalFileName { get; private set; }
|
||||
public long OriginalSizeKb { get; private set; }
|
||||
public string? SignedFileName { get; private set; }
|
||||
public long SignedSizeKb { get; private set; }
|
||||
public string? OutputPath { get; private set; }
|
||||
public List<SignatureInformation>? SignatureInfo { get; private set; }
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (PdfFile is null || PdfFile.Length == 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(PdfFile), "Bitte eine PDF-Datei auswählen.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (!PdfFile.FileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(PdfFile), "Nur PDF-Dateien sind erlaubt.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OriginalFileName = PdfFile.FileName;
|
||||
|
||||
// PDF in Byte-Array laden
|
||||
using var memStream = new MemoryStream();
|
||||
await PdfFile.CopyToAsync(memStream);
|
||||
var originalBytes = memStream.ToArray();
|
||||
OriginalSizeKb = originalBytes.Length / 1024;
|
||||
|
||||
logger.LogInformation("Signiere PDF: {FileName} ({Size} KB)", OriginalFileName, OriginalSizeKb);
|
||||
|
||||
// PDF signieren
|
||||
var signedBytes = await signingService.SignPdfAsync(originalBytes);
|
||||
SignedSizeKb = signedBytes.Length / 1024;
|
||||
|
||||
// Speichern
|
||||
var outputDir = configuration["PdfResults:OutputDirectory"] ?? Path.GetTempPath();
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
SignedFileName = Path.GetFileNameWithoutExtension(PdfFile.FileName) + "_signed.pdf";
|
||||
OutputPath = Path.Combine(outputDir, SignedFileName);
|
||||
|
||||
await System.IO.File.WriteAllBytesAsync(OutputPath, signedBytes);
|
||||
|
||||
logger.LogInformation("Signierte PDF gespeichert: {Path}", OutputPath);
|
||||
|
||||
// Signatur-Informationen auslesen
|
||||
SignatureInfo = await signingService.GetSignatureInfoAsync(signedBytes);
|
||||
|
||||
Success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Fehler beim Signieren der PDF: {FileName}", PdfFile.FileName);
|
||||
ErrorMessage = ex.Message;
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ builder.Services.AddScoped<ZugferdExtractorService>();
|
||||
builder.Services.AddScoped<ZugferdParserService>();
|
||||
builder.Services.AddScoped<ZugferdImportService>();
|
||||
builder.Services.AddScoped<AttachmentViewerService>();
|
||||
builder.Services.AddScoped<PdfSigningService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
183
DXApp.TemplateKitProject/Services/PdfSigningService.cs
Normal file
183
DXApp.TemplateKitProject/Services/PdfSigningService.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using DevExpress.Pdf;
|
||||
using DevExpress.Office.DigitalSignatures;
|
||||
|
||||
namespace DXApp.TemplateKitProject.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service zum digitalen Signieren von PDF-Dokumenten mit DevExpress PdfDocumentSigner
|
||||
/// </summary>
|
||||
public class PdfSigningService(
|
||||
IConfiguration configuration,
|
||||
ILogger<PdfSigningService> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// Signiert eine PDF-Datei mit einem PFX-Zertifikat
|
||||
/// </summary>
|
||||
public async Task<byte[]> SignPdfAsync(byte[] pdfBytes, PdfSigningOptions? options = null)
|
||||
{
|
||||
options ??= new PdfSigningOptions();
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Zertifikat-Konfiguration laden
|
||||
var certPath = options.CertificatePath ?? configuration["PdfSigning:CertificatePath"]
|
||||
?? throw new InvalidOperationException("PdfSigning:CertificatePath nicht konfiguriert.");
|
||||
|
||||
var certPassword = options.CertificatePassword ?? configuration["PdfSigning:CertificatePassword"]
|
||||
?? throw new InvalidOperationException("PdfSigning:CertificatePassword nicht konfiguriert.");
|
||||
|
||||
logger.LogInformation("PDF wird signiert mit Zertifikat: {Path}", certPath);
|
||||
|
||||
// PDF in MemoryStream laden
|
||||
using var inputStream = new MemoryStream(pdfBytes);
|
||||
using var signer = new PdfDocumentSigner(inputStream);
|
||||
|
||||
// PKCS#7 Signatur erstellen
|
||||
var pkcs7Signature = new Pkcs7Signer(certPath, certPassword, HashAlgorithmType.SHA256);
|
||||
|
||||
// Signatur-Feld definieren
|
||||
var fieldInfo = new PdfSignatureFieldInfo(signer.PageCount - 1); // Letzte Seite
|
||||
fieldInfo.Name = $"Signature_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
fieldInfo.SignatureBounds = new PdfRectangle(
|
||||
options.SignatureX,
|
||||
options.SignatureY,
|
||||
options.SignatureX + options.SignatureWidth,
|
||||
options.SignatureY + options.SignatureHeight
|
||||
);
|
||||
|
||||
// Signatur-Builder erstellen
|
||||
var signatureBuilder = new PdfSignatureBuilder(pkcs7Signature, fieldInfo);
|
||||
signatureBuilder.Name = options.SignerName;
|
||||
signatureBuilder.Location = options.Location;
|
||||
signatureBuilder.Reason = options.Reason;
|
||||
|
||||
// Optional: Signatur-Bild hinzufügen
|
||||
if (!string.IsNullOrEmpty(options.SignatureImagePath) && File.Exists(options.SignatureImagePath))
|
||||
{
|
||||
signatureBuilder.SetImageData(File.ReadAllBytes(options.SignatureImagePath));
|
||||
logger.LogDebug("Signatur-Bild hinzugefügt: {Path}", options.SignatureImagePath);
|
||||
}
|
||||
|
||||
// Signierte PDF in MemoryStream speichern
|
||||
using var outputStream = new MemoryStream();
|
||||
signer.SaveDocument(outputStream, signatureBuilder);
|
||||
|
||||
logger.LogInformation(
|
||||
"PDF erfolgreich signiert. Größe: {Original} KB ? {Signed} KB",
|
||||
pdfBytes.Length / 1024,
|
||||
outputStream.Length / 1024
|
||||
);
|
||||
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Fehler beim Signieren der PDF-Datei.");
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest Signatur-Informationen aus einer signierten PDF
|
||||
/// </summary>
|
||||
public async Task<List<SignatureInformation>> GetSignatureInfoAsync(byte[] pdfBytes)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = new MemoryStream(pdfBytes);
|
||||
using var signer = new PdfDocumentSigner(stream);
|
||||
|
||||
var signatures = signer.GetSignatureInfo();
|
||||
var result = new List<SignatureInformation>();
|
||||
|
||||
foreach (var sig in signatures)
|
||||
{
|
||||
var info = new SignatureInformation
|
||||
{
|
||||
FieldName = sig.FieldName,
|
||||
SignerName = sig.SignerName,
|
||||
Location = sig.Location,
|
||||
Reason = sig.Reason,
|
||||
Date = DateTime.Now, // Wird später aus PKCS#7 gelesen
|
||||
CertificationLevel = sig.CertificationLevel.ToString()
|
||||
};
|
||||
|
||||
// PKCS#7 Details auslesen
|
||||
try
|
||||
{
|
||||
var pkcs7 = signer.GetPdfPkcs7Signature(sig.FieldName);
|
||||
var cert = pkcs7.GetSignatureCertificate();
|
||||
|
||||
// Signatur-Datum: GetTimeStampDate oder aktuelles Datum
|
||||
var timestampDate = pkcs7.GetTimeStampDate();
|
||||
info.Date = timestampDate ?? DateTime.Now;
|
||||
|
||||
info.IsSignatureValid = pkcs7.VerifySignature();
|
||||
info.IsCertificateValid = cert.Verify();
|
||||
info.CertificateIssuer = cert.IssuerName.Name ?? string.Empty;
|
||||
info.CertificateSubject = cert.SubjectName.Name ?? string.Empty;
|
||||
info.CertificateValidUntil = cert.NotAfter;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Fehler beim Auslesen der PKCS#7-Details für Signatur '{Name}'", sig.FieldName);
|
||||
}
|
||||
|
||||
result.Add(info);
|
||||
}
|
||||
|
||||
logger.LogInformation("PDF enthält {Count} Signatur(en)", result.Count);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Fehler beim Auslesen der Signatur-Informationen.");
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optionen für die PDF-Signatur
|
||||
/// </summary>
|
||||
public class PdfSigningOptions
|
||||
{
|
||||
public string? CertificatePath { get; set; }
|
||||
public string? CertificatePassword { get; set; }
|
||||
public string SignerName { get; set; } = "DXApp System";
|
||||
public string Location { get; set; } = "Digital Data AG";
|
||||
public string Reason { get; set; } = "Dokument digital signiert";
|
||||
|
||||
// Position und Größe der sichtbaren Signatur (in Points, 72 DPI)
|
||||
public double SignatureX { get; set; } = 50;
|
||||
public double SignatureY { get; set; } = 50;
|
||||
public double SignatureWidth { get; set; } = 200;
|
||||
public double SignatureHeight { get; set; } = 80;
|
||||
|
||||
public string? SignatureImagePath { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informationen über eine PDF-Signatur
|
||||
/// </summary>
|
||||
public class SignatureInformation
|
||||
{
|
||||
public string FieldName { get; set; } = string.Empty;
|
||||
public string SignerName { get; set; } = string.Empty;
|
||||
public string Location { get; set; } = string.Empty;
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
public DateTime Date { get; set; }
|
||||
public string CertificationLevel { get; set; } = string.Empty;
|
||||
|
||||
public bool IsSignatureValid { get; set; }
|
||||
public bool IsCertificateValid { get; set; }
|
||||
public string CertificateIssuer { get; set; } = string.Empty;
|
||||
public string CertificateSubject { get; set; } = string.Empty;
|
||||
public DateTime CertificateValidUntil { get; set; }
|
||||
}
|
||||
@@ -17,5 +17,11 @@
|
||||
},
|
||||
"PdfResults": {
|
||||
"OutputDirectory": "C:\\PdfResults"
|
||||
},
|
||||
"PdfSigning": {
|
||||
"CertificatePath": "C:\\PdfCertificates\\dxapp-test.pfx",
|
||||
"CertificatePassword": "DXAppTest123!",
|
||||
"SignerName": "DXApp Test System",
|
||||
"Location": "Digital Data AG"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user