Improve PDF signing and validation logic
Updated `.gitignore` to exclude new files and directories, including `FodyWeavers.xsd` and `appsettings.Secrets.json`. Refactored `PdfDigitalSignatureService`: - Improved signing time handling by calculating fallback inline. - Simplified `SignPdfAsync` by inlining signature building. - Added `ParseByteRanges` to extract `/ByteRange` values from PDFs. - Introduced `CoversWholeDocument` to validate document coverage. Enhanced signature validation: - Integrated `ParseByteRanges` and `CoversWholeDocument`. - Streamlined `PdfSignatureCheckItemDto` construction. - Updated properties to calculate values inline. - Improved `Unchanged` property to check cryptographic validity and document coverage. These changes enhance maintainability, readability, and functionality.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -370,3 +370,4 @@ FodyWeavers.xsd
|
|||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
/ignore/EnvelopeGenerator.Form
|
/ignore/EnvelopeGenerator.Form
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System.Security.Cryptography.Pkcs;
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
|
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
|
||||||
|
|
||||||
@@ -20,7 +21,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
|||||||
ArgumentNullException.ThrowIfNull(pdfBytes);
|
ArgumentNullException.ThrowIfNull(pdfBytes);
|
||||||
var pkcs11Options = options.Value;
|
var pkcs11Options = options.Value;
|
||||||
var request = optionsModel ?? new PdfSignOptionsDto();
|
var request = optionsModel ?? new PdfSignOptionsDto();
|
||||||
var fallbackSigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
|
|
||||||
|
|
||||||
using var inputPdf = new MemoryStream(pdfBytes);
|
using var inputPdf = new MemoryStream(pdfBytes);
|
||||||
using var signer = new PdfDocumentSigner(inputPdf);
|
using var signer = new PdfDocumentSigner(inputPdf);
|
||||||
@@ -38,7 +38,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
|||||||
Reason = request.Reason ?? pkcs11Options.DefaultReason,
|
Reason = request.Reason ?? pkcs11Options.DefaultReason,
|
||||||
Location = request.Location,
|
Location = request.Location,
|
||||||
ContactInfo = request.ContactInfo,
|
ContactInfo = request.ContactInfo,
|
||||||
SigningTime = request.SigningTime ?? fallbackSigningTime,
|
SigningTime = request.SigningTime ?? (pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now),
|
||||||
ApplicationName = request.ApplicationName ?? pkcs11Options.DefaultApplicationName,
|
ApplicationName = request.ApplicationName ?? pkcs11Options.DefaultApplicationName,
|
||||||
CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel)
|
CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel)
|
||||||
?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel)
|
?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel)
|
||||||
@@ -47,10 +47,9 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
|||||||
|
|
||||||
var deferredSigner = signer.SignDeferred(builder);
|
var deferredSigner = signer.SignDeferred(builder);
|
||||||
var externalSigner = new Pkcs11ExternalSigner(pkcs11Options);
|
var externalSigner = new Pkcs11ExternalSigner(pkcs11Options);
|
||||||
byte[] signature = externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid);
|
|
||||||
|
|
||||||
using var outputPdf = new MemoryStream();
|
using var outputPdf = new MemoryStream();
|
||||||
deferredSigner.Sign(outputPdf, signature);
|
deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid));
|
||||||
return Task.FromResult(outputPdf.ToArray());
|
return Task.FromResult(outputPdf.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +79,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
|||||||
using var inputPdf = new MemoryStream(pdfBytes);
|
using var inputPdf = new MemoryStream(pdfBytes);
|
||||||
using var signer = new PdfDocumentSigner(inputPdf);
|
using var signer = new PdfDocumentSigner(inputPdf);
|
||||||
var signatures = signer.GetSignatureInfo();
|
var signatures = signer.GetSignatureInfo();
|
||||||
|
var byteRanges = ParseByteRanges(pdfBytes);
|
||||||
|
|
||||||
if (signatures is null || signatures.Count == 0)
|
if (signatures is null || signatures.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -90,25 +90,18 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
|||||||
|
|
||||||
var items = new List<PdfSignatureCheckItemDto>(signatures.Count);
|
var items = new List<PdfSignatureCheckItemDto>(signatures.Count);
|
||||||
|
|
||||||
foreach (var signature in signatures)
|
for (var i = 0; i < signatures.Count; i++)
|
||||||
{
|
{
|
||||||
|
var signature = signatures[i];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pkcs7 = signer.GetPdfPkcs7Signature(signature.FieldName);
|
var pkcs7 = signer.GetPdfPkcs7Signature(signature.FieldName);
|
||||||
bool valid = pkcs7.VerifySignature();
|
bool valid = pkcs7.VerifySignature();
|
||||||
bool coversWholeDocument = true;
|
var byteRange = i < byteRanges.Count ? byteRanges[i] : null;
|
||||||
|
var coversWholeDocument = CoversWholeDocument(byteRange, pdfBytes.LongLength);
|
||||||
var certificate = pkcs7.GetSignatureCertificate();
|
var certificate = pkcs7.GetSignatureCertificate();
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
var serialNumber = ToColonSerial(certificate?.SerialNumber);
|
|
||||||
DateTimeOffset? validFrom = certificate is null ? null : new DateTimeOffset(certificate.NotBefore).ToUniversalTime();
|
|
||||||
DateTimeOffset? validTo = certificate is null ? null : new DateTimeOffset(certificate.NotAfter).ToUniversalTime();
|
|
||||||
var issuedTo = ParsePrincipal(certificate?.Subject);
|
var issuedTo = ParsePrincipal(certificate?.Subject);
|
||||||
var issuedBy = ParsePrincipal(certificate?.Issuer);
|
var issuedBy = ParsePrincipal(certificate?.Issuer);
|
||||||
var certificateInfo = BuildCertificateInfo(certificate, now);
|
|
||||||
var digestAlgorithm = InferDigestAlgorithm(certificate);
|
|
||||||
var signatureAlgorithm = InferSignatureAlgorithm(certificate);
|
|
||||||
var signerDisplayName = issuedTo.CommonName ?? signature.SignerName;
|
|
||||||
var messageDigestHex = TryExtractMessageDigestHex(signature.Contents);
|
|
||||||
|
|
||||||
items.Add(new PdfSignatureCheckItemDto(
|
items.Add(new PdfSignatureCheckItemDto(
|
||||||
CoversWholeDoc: coversWholeDocument,
|
CoversWholeDoc: coversWholeDocument,
|
||||||
@@ -116,23 +109,23 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
|||||||
Location: signature.Location,
|
Location: signature.Location,
|
||||||
DeclaredName: signature.SignerName,
|
DeclaredName: signature.SignerName,
|
||||||
ContactInfo: signature.ContactInfo,
|
ContactInfo: signature.ContactInfo,
|
||||||
DigestAlgorithm: digestAlgorithm,
|
DigestAlgorithm: InferDigestAlgorithm(certificate),
|
||||||
SignatureAlgorithm: signatureAlgorithm,
|
SignatureAlgorithm: InferSignatureAlgorithm(certificate),
|
||||||
SignerName: signerDisplayName,
|
SignerName: issuedTo.CommonName ?? signature.SignerName,
|
||||||
Issuer: issuedBy.Full,
|
Issuer: issuedBy.Full,
|
||||||
SerialNumber: serialNumber,
|
SerialNumber: ToColonSerial(certificate?.SerialNumber),
|
||||||
ValidFrom: validFrom,
|
ValidFrom: certificate is null ? null : new DateTimeOffset(certificate.NotBefore).ToUniversalTime(),
|
||||||
ValidTo: validTo,
|
ValidTo: certificate is null ? null : new DateTimeOffset(certificate.NotAfter).ToUniversalTime(),
|
||||||
ExpiryCheckedAgainst: "now",
|
ExpiryCheckedAgainst: "now",
|
||||||
Certificates: [certificateInfo],
|
Certificates: [BuildCertificateInfo(certificate, DateTimeOffset.UtcNow)],
|
||||||
Unchanged: valid,
|
Unchanged: valid && coversWholeDocument,
|
||||||
FieldName: signature.FieldName,
|
FieldName: signature.FieldName,
|
||||||
IsCryptographicallyValid: valid,
|
IsCryptographicallyValid: valid,
|
||||||
SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"),
|
SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"),
|
||||||
Filter: signature.Filter,
|
Filter: signature.Filter,
|
||||||
SubFilter: signature.SubFilter,
|
SubFilter: signature.SubFilter,
|
||||||
ApplicationName: null,
|
ApplicationName: null,
|
||||||
MessageDigestHex: messageDigestHex,
|
MessageDigestHex: TryExtractMessageDigestHex(signature.Contents),
|
||||||
Error: null));
|
Error: null));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -350,4 +343,59 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<long[]> ParseByteRanges(byte[] pdfBytes)
|
||||||
|
{
|
||||||
|
var text = Encoding.Latin1.GetString(pdfBytes);
|
||||||
|
var matches = Regex.Matches(text, @"/ByteRange\s*\[(?<values>[\d\s]+)\]", RegexOptions.CultureInvariant);
|
||||||
|
var ranges = new List<long[]>(matches.Count);
|
||||||
|
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
var raw = match.Groups["values"].Value;
|
||||||
|
var parts = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var values = new long[parts.Length];
|
||||||
|
var ok = true;
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
if (!long.TryParse(parts[i], out values[i]))
|
||||||
|
{
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok && values.Length >= 4 && values.Length % 2 == 0)
|
||||||
|
ranges.Add(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CoversWholeDocument(long[]? byteRange, long fileLength)
|
||||||
|
{
|
||||||
|
if (byteRange is null || byteRange.Length < 4 || byteRange.Length % 2 != 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (byteRange[0] != 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
long previousEnd = -1;
|
||||||
|
for (var i = 0; i < byteRange.Length; i += 2)
|
||||||
|
{
|
||||||
|
var start = byteRange[i];
|
||||||
|
var length = byteRange[i + 1];
|
||||||
|
if (start < 0 || length < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var end = start + length;
|
||||||
|
if (previousEnd > start)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
previousEnd = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousEnd == fileLength;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user