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:
@@ -10,6 +10,7 @@ using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
|
||||
|
||||
@@ -20,7 +21,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
ArgumentNullException.ThrowIfNull(pdfBytes);
|
||||
var pkcs11Options = options.Value;
|
||||
var request = optionsModel ?? new PdfSignOptionsDto();
|
||||
var fallbackSigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
|
||||
|
||||
using var inputPdf = new MemoryStream(pdfBytes);
|
||||
using var signer = new PdfDocumentSigner(inputPdf);
|
||||
@@ -38,7 +38,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
Reason = request.Reason ?? pkcs11Options.DefaultReason,
|
||||
Location = request.Location,
|
||||
ContactInfo = request.ContactInfo,
|
||||
SigningTime = request.SigningTime ?? fallbackSigningTime,
|
||||
SigningTime = request.SigningTime ?? (pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now),
|
||||
ApplicationName = request.ApplicationName ?? pkcs11Options.DefaultApplicationName,
|
||||
CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel)
|
||||
?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel)
|
||||
@@ -47,10 +47,9 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
|
||||
var deferredSigner = signer.SignDeferred(builder);
|
||||
var externalSigner = new Pkcs11ExternalSigner(pkcs11Options);
|
||||
byte[] signature = externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid);
|
||||
|
||||
using var outputPdf = new MemoryStream();
|
||||
deferredSigner.Sign(outputPdf, signature);
|
||||
deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid));
|
||||
return Task.FromResult(outputPdf.ToArray());
|
||||
}
|
||||
|
||||
@@ -80,6 +79,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
using var inputPdf = new MemoryStream(pdfBytes);
|
||||
using var signer = new PdfDocumentSigner(inputPdf);
|
||||
var signatures = signer.GetSignatureInfo();
|
||||
var byteRanges = ParseByteRanges(pdfBytes);
|
||||
|
||||
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);
|
||||
|
||||
foreach (var signature in signatures)
|
||||
for (var i = 0; i < signatures.Count; i++)
|
||||
{
|
||||
var signature = signatures[i];
|
||||
try
|
||||
{
|
||||
var pkcs7 = signer.GetPdfPkcs7Signature(signature.FieldName);
|
||||
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 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 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(
|
||||
CoversWholeDoc: coversWholeDocument,
|
||||
@@ -116,23 +109,23 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
Location: signature.Location,
|
||||
DeclaredName: signature.SignerName,
|
||||
ContactInfo: signature.ContactInfo,
|
||||
DigestAlgorithm: digestAlgorithm,
|
||||
SignatureAlgorithm: signatureAlgorithm,
|
||||
SignerName: signerDisplayName,
|
||||
DigestAlgorithm: InferDigestAlgorithm(certificate),
|
||||
SignatureAlgorithm: InferSignatureAlgorithm(certificate),
|
||||
SignerName: issuedTo.CommonName ?? signature.SignerName,
|
||||
Issuer: issuedBy.Full,
|
||||
SerialNumber: serialNumber,
|
||||
ValidFrom: validFrom,
|
||||
ValidTo: validTo,
|
||||
SerialNumber: ToColonSerial(certificate?.SerialNumber),
|
||||
ValidFrom: certificate is null ? null : new DateTimeOffset(certificate.NotBefore).ToUniversalTime(),
|
||||
ValidTo: certificate is null ? null : new DateTimeOffset(certificate.NotAfter).ToUniversalTime(),
|
||||
ExpiryCheckedAgainst: "now",
|
||||
Certificates: [certificateInfo],
|
||||
Unchanged: valid,
|
||||
Certificates: [BuildCertificateInfo(certificate, DateTimeOffset.UtcNow)],
|
||||
Unchanged: valid && coversWholeDocument,
|
||||
FieldName: signature.FieldName,
|
||||
IsCryptographicallyValid: valid,
|
||||
SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"),
|
||||
Filter: signature.Filter,
|
||||
SubFilter: signature.SubFilter,
|
||||
ApplicationName: null,
|
||||
MessageDigestHex: messageDigestHex,
|
||||
MessageDigestHex: TryExtractMessageDigestHex(signature.Contents),
|
||||
Error: null));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -350,4 +343,59 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
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