diff --git a/DXApp.TemplateKitProject/Pages/TestSignature.cshtml b/DXApp.TemplateKitProject/Pages/TestSignature.cshtml new file mode 100644 index 0000000..a2ac8da --- /dev/null +++ b/DXApp.TemplateKitProject/Pages/TestSignature.cshtml @@ -0,0 +1,90 @@ +@page +@model DXApp.TemplateKitProject.Pages.TestSignatureModel +@{ + ViewData["Title"] = "PDF-Signatur testen"; +} + +
+

PDF-Signatur Test

+

Einfacher Test zum Signieren einer PDF mit dem DevExpress PdfDocumentSigner.

+ +
+
+ + + +
+ + +
+ + @if (Model.Success) + { +
+
? PDF erfolgreich signiert!
+

+ Original: @Model.OriginalFileName (@Model.OriginalSizeKb KB)
+ Signiert: @Model.SignedFileName (@Model.SignedSizeKb KB)
+ Gespeichert: @Model.OutputPath +

+
+ + @if (Model.SignatureInfo != null && Model.SignatureInfo.Count > 0) + { +
+
Signatur-Details:
+ @foreach (var sig in Model.SignatureInfo) + { +
+ Signatur: @sig.FieldName
+ Unterzeichner: @sig.SignerName
+ Ort: @sig.Location
+ Grund: @sig.Reason
+ Datum: @sig.Date.ToString("dd.MM.yyyy HH:mm:ss")
+ Signatur gültig: + @if (sig.IsSignatureValid) + { + ? Ja + } + else + { + ? Nein + } +
+ Zertifikat gültig: + @if (sig.IsCertificateValid) + { + ? Ja + } + else + { + ? Nein + } +
+ Zertifikat gültig bis: @sig.CertificateValidUntil.ToString("dd.MM.yyyy")
+ Issuer: @sig.CertificateIssuer +
+ } +
+ } + } + + @if (!string.IsNullOrEmpty(Model.ErrorMessage)) + { +
+ Fehler: @Model.ErrorMessage +
+ } + +
+
?? Test-Schritte:
+
    +
  1. PDF-Datei auswählen (z.B. eine ZUGFeRD-Rechnung aus /Invoices)
  2. +
  3. Auf "PDF signieren" klicken
  4. +
  5. Signierte PDF wird in C:\PdfResults gespeichert
  6. +
  7. Mit Adobe Acrobat Reader öffnen und Signatur prüfen
  8. +
+
+
diff --git a/DXApp.TemplateKitProject/Pages/TestSignature.cshtml.cs b/DXApp.TemplateKitProject/Pages/TestSignature.cshtml.cs new file mode 100644 index 0000000..5586843 --- /dev/null +++ b/DXApp.TemplateKitProject/Pages/TestSignature.cshtml.cs @@ -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 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? SignatureInfo { get; private set; } + + public void OnGet() + { + } + + public async Task 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(); + } +} diff --git a/DXApp.TemplateKitProject/Program.cs b/DXApp.TemplateKitProject/Program.cs index 3e4a6af..b18ed9e 100644 --- a/DXApp.TemplateKitProject/Program.cs +++ b/DXApp.TemplateKitProject/Program.cs @@ -19,6 +19,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); diff --git a/DXApp.TemplateKitProject/Services/PdfSigningService.cs b/DXApp.TemplateKitProject/Services/PdfSigningService.cs new file mode 100644 index 0000000..056b159 --- /dev/null +++ b/DXApp.TemplateKitProject/Services/PdfSigningService.cs @@ -0,0 +1,183 @@ +using DevExpress.Pdf; +using DevExpress.Office.DigitalSignatures; + +namespace DXApp.TemplateKitProject.Services; + +/// +/// Service zum digitalen Signieren von PDF-Dokumenten mit DevExpress PdfDocumentSigner +/// +public class PdfSigningService( + IConfiguration configuration, + ILogger logger) +{ + /// + /// Signiert eine PDF-Datei mit einem PFX-Zertifikat + /// + public async Task 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; + } + }); + } + + /// + /// Liest Signatur-Informationen aus einer signierten PDF + /// + public async Task> 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(); + + 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; + } + }); + } +} + +/// +/// Optionen für die PDF-Signatur +/// +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; } +} + +/// +/// Informationen über eine PDF-Signatur +/// +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; } +} diff --git a/DXApp.TemplateKitProject/appsettings.json b/DXApp.TemplateKitProject/appsettings.json index 8fb61d4..f564040 100644 --- a/DXApp.TemplateKitProject/appsettings.json +++ b/DXApp.TemplateKitProject/appsettings.json @@ -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" } } \ No newline at end of file