From adaf57e3d040b19569588000dbd4338d426e6def Mon Sep 17 00:00:00 2001 From: Julien R Date: Wed, 18 Oct 2023 15:19:02 +0200 Subject: [PATCH 01/20] expose PdfString's start and end bytes positions in Stream This will make possible the implementation of document signature, because we need to exclude this specific byte range from the hash digest. --- .../src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs index eccb3cae..36a8920c 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs @@ -284,7 +284,21 @@ static bool IsRawEncoding(string s) /// internal override void WriteObject(PdfWriter writer) { + PositionStart = writer.Position; + writer.Write(this); + + PositionEnd = writer.Position; } + + /// + /// Position of the first byte of this string in PdfWriter's Stream + /// + public int PositionStart { get; internal set; } + + /// + /// Position of the last byte of this string in PdfWriter's Stream + /// + public int PositionEnd { get; internal set; } } } From a545b4c995f4ae422a481e20eb8e1806021cebea Mon Sep 17 00:00:00 2001 From: Julien R Date: Fri, 20 Oct 2023 15:56:38 +0200 Subject: [PATCH 02/20] add PKCS#7 detached signature feature allows to sign pdf document using PKCS#7 detached format, digest algorithm should be SHA1 (only one supported in PDF 1.4) --- src/Directory.Packages.props | 1 + .../IAnnotationAppearanceHandler.cs | 12 + .../DefaultSignatureAppearanceHandler.cs | 35 +++ .../PdfSharp/Pdf.Signatures/DefaultSigner.cs | 42 ++++ .../src/PdfSharp/Pdf.Signatures/ISigner.cs | 12 + .../Pdf.Signatures/PdfArrayWithPadding.cs | 46 ++++ .../Pdf.Signatures/PdfSignatureHandler.cs | 220 ++++++++++++++++++ .../Pdf.Signatures/PdfSignatureOptions.cs | 17 ++ .../PdfSharp/Pdf.Signatures/RangedStream.cs | 164 +++++++++++++ .../PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 19 +- .../src/PDFsharp/src/PdfSharp/PdfSharp.csproj | 4 + 11 files changed, 571 insertions(+), 1 deletion(-) create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/IAnnotationAppearanceHandler.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureAppearanceHandler.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 9e14947d..3e782c0c 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -134,6 +134,7 @@ + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/IAnnotationAppearanceHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/IAnnotationAppearanceHandler.cs new file mode 100644 index 00000000..e686b05f --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/IAnnotationAppearanceHandler.cs @@ -0,0 +1,12 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using PdfSharp.Drawing; + +namespace PdfSharp.Pdf.Annotations +{ + public interface IAnnotationAppearanceHandler + { + void DrawAppearance(XGraphics gfx, XRect rect); + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureAppearanceHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureAppearanceHandler.cs new file mode 100644 index 00000000..652e9cfa --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureAppearanceHandler.cs @@ -0,0 +1,35 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using PdfSharp.Drawing; +using PdfSharp.Drawing.Layout; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.Signatures +{ + internal class DefaultSignatureAppearanceHandler : IAnnotationAppearanceHandler + { + public string? Location { get; set; } + public string? Reason { get; set; } + public string? Signer { get; set; } + + + public void DrawAppearance(XGraphics gfx, XRect rect) + { + var backColor = XColor.Empty; + var defaultText = string.Format("Signed by: {0}\nLocation: {1}\nReason: {2}\nDate: {3}", Signer, Location, Reason, DateTime.Now); + + XFont font = new XFont("Verdana", 7, XFontStyleEx.Regular); + + XTextFormatter txtFormat = new XTextFormatter(gfx); + + var currentPosition = new XPoint(0, 0); + + txtFormat.DrawString(defaultText, + font, + new XSolidBrush(XColor.FromKnownColor(XKnownColor.Black)), + new XRect(currentPosition.X, currentPosition.Y, rect.Width - currentPosition.X, rect.Height), + XStringFormats.TopLeft); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs new file mode 100644 index 00000000..aaaaa0ae --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs @@ -0,0 +1,42 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using System.Security.Cryptography.Pkcs; +using System.Security.Cryptography.X509Certificates; + +namespace PdfSharp.Pdf.Signatures +{ + public class DefaultSigner : ISigner + { + public X509Certificate2 Certificate { get; private set; } + + public DefaultSigner(X509Certificate2 Certificate) + { + this.Certificate = Certificate; + } + + public byte[] GetSignedCms(Stream stream, string recommendedDigestAlgorithm) + { + var range = new byte[stream.Length]; + + stream.Position = 0; + stream.Read(range, 0, range.Length); + + var contentInfo = new ContentInfo(range); + + SignedCms signedCms = new SignedCms(contentInfo, true); + CmsSigner signer = new CmsSigner(Certificate); + signer.UnsignedAttributes.Add(new Pkcs9SigningTime()); + + signedCms.ComputeSignature(signer, true); + var bytes = signedCms.Encode(); + + return bytes; + } + + public string GetName() + { + return Certificate.GetNameInfo(X509NameType.SimpleName, false); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs new file mode 100644 index 00000000..0d043495 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs @@ -0,0 +1,12 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +namespace PdfSharp.Pdf.Signatures +{ + public interface ISigner + { + byte[] GetSignedCms(Stream stream, string recommendedDigestAlgorithm); + + string GetName(); + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs new file mode 100644 index 00000000..9a962279 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs @@ -0,0 +1,46 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using PdfSharp.Pdf.IO; + +namespace PdfSharp.Pdf.Signatures +{ + internal class PdfArrayWithPadding : PdfArray + { + public int PaddingRight { get; private set; } + + public PdfArrayWithPadding(PdfDocument document, int paddingRight, params PdfItem[] items) + : base(document, items) + { + PaddingRight = paddingRight; + } + + internal override void WriteObject(PdfWriter writer) + { + PositionStart = writer.Position; + + base.WriteObject(writer); + + if (PaddingRight > 0) + { + var bytes = new byte[PaddingRight]; + for (int i = 0; i < PaddingRight; i++) + bytes[i] = 32;// space + + writer.Write(bytes); + } + + PositionEnd = writer.Position; + } + + /// + /// Position of the first byte of this string in PdfWriter's Stream + /// + public int PositionStart { get; internal set; } + + /// + /// Position of the last byte of this string in PdfWriter's Stream + /// + public int PositionEnd { get; internal set; } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs new file mode 100644 index 00000000..d83ca483 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -0,0 +1,220 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using PdfSharp.Drawing; +using PdfSharp.Pdf.AcroForms; +using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Annotations; +using System.Text; + +namespace PdfSharp.Pdf.Signatures +{ + public class IntEventArgs : EventArgs { public int Value { get; set; } } + + /// + /// PdfDocument signature handler. + /// Attaches a PKCS#7 signature digest to PdfDocument. PdfSharp currently supports PDF 1.4 documents, so digest algorithm should be SHA1 (supported in every subsequent version anyway): + /// adbe.pkcs7.detached supported algorithms: SHA1 (PDF 1.3), SHA256 (PDF 1.6), SHA384/SHA512/RIPEMD160 (PDF 1.7) + /// + public class PdfSignatureHandler + { + const string SupportedDigestAlgorithm = "SHA1"; // adbe.pkcs7.detached supported algorithms: SHA1 (PDF 1.3), SHA256 (PDF 1.6), SHA384/SHA512/RIPEMD160 (PDF 1.7) + + private PdfString signatureFieldContents; + private PdfArray signatureFieldByteRange; + + private int? maximumSignatureLength; + private const int byteRangePaddingLength = 36; // place big enough required to replace [0 0 0 0] with the correct value + + public event EventHandler SignatureSizeComputed = (s, e) => { }; + + public PdfDocument Document { get; private set; } + public PdfSignatureOptions Options { get; private set; } + private ISigner signer { get; set; } + + public void AttachToDocument(PdfDocument documentToSign) + { + this.Document = documentToSign; + this.Document.BeforeSave += AddSignatureComponents; + this.Document.AfterSave += ComputeSignatureAndRange; + + if (!maximumSignatureLength.HasValue) + { + maximumSignatureLength = signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), SupportedDigestAlgorithm).Length; + SignatureSizeComputed(this, new IntEventArgs() { Value = maximumSignatureLength.Value }); + } + } + + public PdfSignatureHandler(ISigner signer, int? signatureMaximumLength, PdfSignatureOptions options) + { + this.signer = signer; + this.maximumSignatureLength = signatureMaximumLength; + this.Options = options; + } + + private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) + { + var writer = e.Writer; + + // writing actual ByteRange in place of the placeholder + + var rangeArray = new PdfArray(); + rangeArray.Elements.Add(new PdfInteger(0)); + rangeArray.Elements.Add(new PdfInteger(signatureFieldContents.PositionStart)); + rangeArray.Elements.Add(new PdfInteger(signatureFieldContents.PositionEnd)); + rangeArray.Elements.Add(new PdfInteger((int)writer.Stream.Length - signatureFieldContents.PositionEnd)); + + writer.Stream.Position = (signatureFieldByteRange as PdfArrayWithPadding).PositionStart; + rangeArray.WriteObject(writer); + + + // computing and writing document's digest + + var rangeToSign = GetRangeToSign(writer.Stream); // will exclude SignatureField's /Contents from hash computation + + var digest = signer.GetSignedCms(rangeToSign, SupportedDigestAlgorithm); + if (digest.Length > maximumSignatureLength) + throw new Exception("The digest length is bigger that the approximation made."); + + var hexFormatedDigest = Encoding.Default.GetBytes(FormatHex(digest)); + + writer.Stream.Position = signatureFieldContents.PositionStart + 1/*' '*/ + 1/*'<'*/; // PositionStart starts right after /Contents, so we take into account the space separator and the starting '<' before writing the hash + writer.Write(hexFormatedDigest); + } + + string FormatHex(byte[] bytes) // starting from .net5, could be replaced by Convert.ToHexString(Byte[]). keeping current method to be ease .net48 compatibility + { + var retval = new StringBuilder(); + + for (int idx = 0; idx < bytes.Length; idx++) + retval.AppendFormat("{0:x2}", bytes[idx]); + + return retval.ToString(); + } + + private RangedStream GetRangeToSign(Stream stream) + { + return new RangedStream(stream, new List() + { + new RangedStream.Range(0, signatureFieldContents.PositionStart), + new RangedStream.Range(signatureFieldContents.PositionEnd, stream.Length - signatureFieldContents.PositionEnd) + }); + } + + private void AddSignatureComponents(object sender, EventArgs e) + { + var hashPlaceholderValue = new String('0', maximumSignatureLength.Value); + signatureFieldContents = new PdfString(hashPlaceholderValue, PdfStringFlags.HexLiteral); + signatureFieldByteRange = new PdfArrayWithPadding(Document, byteRangePaddingLength, new PdfInteger(0), new PdfInteger(0), new PdfInteger(0), new PdfInteger(0)); + //Document.Internals.AddObject(signatureFieldByteRange); + + var signatureDictionary = GetSignatureDictionary(signatureFieldContents, signatureFieldByteRange); + var signatureField = GetSignatureField(signatureDictionary); + RenderAppearance(signatureField, Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() + { + Location = Options.Location, + Reason = Options.Reason, + Signer = signer.GetName() + }); + + var annotations = Document.Pages[0].Elements.GetArray(PdfPage.Keys.Annots); + if (annotations == null) + Document.Pages[0].Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField)); + else + annotations.Elements.Add(signatureField); + + + // acroform + + var catalog = Document.Catalog; + + if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) + catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); + + if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) + catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3)); + else + { + var sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); + if (sigFlagVersion < 3) + catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + } + + if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null) + catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, new PdfArray()); + catalog.AcroForm.Fields.Elements.Add(signatureField); + } + + private PdfSignatureField GetSignatureField(PdfDictionary signatureDic) + { + var signatureField = new PdfSignatureField(Document); + + signatureField.Elements.Add(PdfSignatureField.Keys.V, signatureDic); + + // annotation keys + signatureField.Elements.Add(PdfSignatureField.Keys.FT, new PdfName("/Sig")); + signatureField.Elements.Add(PdfSignatureField.Keys.T, new PdfString("Signature1")); // TODO if already exists, will it cause error? implement a name choser if yes + signatureField.Elements.Add(PdfSignatureField.Keys.Ff, new PdfInteger(132)); + signatureField.Elements.Add(PdfSignatureField.Keys.DR, new PdfDictionary()); + signatureField.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Annot")); + signatureField.Elements.Add("/Subtype", new PdfName("/Widget")); + signatureField.Elements.Add("/P", Document.Pages[0]); + + signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle)); + + Document.Internals.AddObject(signatureField); + + return signatureField; + } + + private void RenderAppearance(PdfSignatureField signatureField, IAnnotationAppearanceHandler appearanceHandler) + { + PdfRectangle rect = signatureField.Elements.GetRectangle(PdfAnnotation.Keys.Rect); + + var visible = !(rect.X1 + rect.X2 + rect.Y1 + rect.Y2 == 0); + + if (!visible) + return; + + if (appearanceHandler == null) + throw new Exception("AppearanceHandler is null"); + + XForm form = new XForm(Document, rect.Size); + XGraphics gfx = XGraphics.FromForm(form); + + appearanceHandler.DrawAppearance(gfx, rect.ToXRect()); + + form.DrawingFinished(); + + // Get existing or create new appearance dictionary + PdfDictionary ap = signatureField.Elements[PdfAnnotation.Keys.AP] as PdfDictionary; + if (ap == null) + { + ap = new PdfDictionary(Document); + signatureField.Elements[PdfAnnotation.Keys.AP] = ap; + } + + // Set XRef to normal state + ap.Elements["/N"] = form.PdfForm.Reference; + } + + private PdfDictionary GetSignatureDictionary(PdfString contents, PdfArray byteRange) + { + PdfDictionary signatureDic = new PdfDictionary(Document); + + signatureDic.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Sig")); + signatureDic.Elements.Add(PdfSignatureField.Keys.Filter, new PdfName("/Adobe.PPKLite")); + signatureDic.Elements.Add(PdfSignatureField.Keys.SubFilter, new PdfName("/adbe.pkcs7.detached")); + signatureDic.Elements.Add(PdfSignatureField.Keys.M, new PdfDate(DateTime.Now)); + + signatureDic.Elements.Add(PdfSignatureField.Keys.Contents, contents); + signatureDic.Elements.Add(PdfSignatureField.Keys.ByteRange, byteRange); + signatureDic.Elements.Add(PdfSignatureField.Keys.Reason, new PdfString(Options.Reason)); + signatureDic.Elements.Add(PdfSignatureField.Keys.Location, new PdfString(Options.Location)); + + Document.Internals.AddObject(signatureDic); + + return signatureDic; + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs new file mode 100644 index 00000000..ae285b7f --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs @@ -0,0 +1,17 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; + +namespace PdfSharp.Pdf.Signatures +{ + public class PdfSignatureOptions + { + public IAnnotationAppearanceHandler AppearanceHandler { get; set; } + public string ContactInfo { get; set; } + public string Location { get; set; } + public string Reason { get; set; } + public XRect Rectangle { get; set; } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs new file mode 100644 index 00000000..af168a2f --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs @@ -0,0 +1,164 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +namespace PdfSharp.Pdf.Signatures +{ + public class RangedStream : Stream + { + private Range[] ranges; + + public class Range + { + + public Range(long offset, long length) + { + this.Offset = offset; + this.Length = length; + } + public long Offset { get; set; } + public long Length { get; set; } + } + + private Stream stream { get; set; } + + + public RangedStream(Stream originalStrem, List ranges) + { + this.stream = originalStrem; + + long previousPosition = 0; + + this.ranges = ranges.OrderBy(item => item.Offset).ToArray(); + foreach (var range in ranges) + { + if (range.Offset < previousPosition) + throw new Exception("Ranges are not continuous"); + previousPosition = range.Offset + range.Length; + } + } + + + public override bool CanRead => true; + + public override bool CanSeek + { + get + { + throw new NotImplementedException(); + } + } + + public override bool CanWrite + { + get + { + return false; + } + } + + public override long Length + { + get + { + return ranges.Sum(item => item.Length); + } + } + + + private IEnumerable GetPreviousRanges(long position) + { + return ranges.Where(item => item.Offset < position && item.Offset + item.Length < position); + } + + private Range GetCurrentRange(long position) + { + return ranges.FirstOrDefault(item => item.Offset <= position && item.Offset + item.Length > position); + } + + + + public override long Position + { + get + { + return GetPreviousRanges(stream.Position).Sum(item => item.Length) + stream.Position - GetCurrentRange(stream.Position).Offset; + } + + set + { + Range? currentRange = null; + List previousRanges = new List(); + long maxPosition = 0; + foreach (var range in ranges) + { + currentRange = range; + maxPosition += range.Length; + if (maxPosition > value) + break; + previousRanges.Add(range); + } + + long positionInCurrentRange = value - previousRanges.Sum(item => item.Length); + stream.Position = currentRange.Offset + positionInCurrentRange; + } + } + + + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + + var length = stream.Length; + int retVal = 0; + for (int i = 0; i < count; i++) + { + + if (stream.Position == length) + { + break; + } + + PerformSkipIfNeeded(); + retVal += stream.Read(buffer, offset++, 1); + + } + + return retVal; + } + + + private void PerformSkipIfNeeded() + { + var currentRange = GetCurrentRange(stream.Position); + + if (currentRange == null) + stream.Position = GetNextRange().Offset; + } + + private Range GetNextRange() + { + return ranges.OrderBy(item => item.Offset).First(item => item.Offset > stream.Position); + } + + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index c5054814..e440681b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -21,12 +21,25 @@ namespace PdfSharp.Pdf { + internal class PdfDocumentEventArgs : EventArgs + { + public PdfDocumentEventArgs(PdfWriter writer) + { + Writer = writer; + } + + public PdfWriter Writer { get; set; } + } + /// /// Represents a PDF document. /// [DebuggerDisplay("(Name={" + nameof(Name) + "})")] // A name makes debugging easier public sealed class PdfDocument : PdfObject, IDisposable { + internal event EventHandler BeforeSave = (s, e) => { }; + internal event EventHandler AfterSave = (s, e) => { }; + internal DocumentState _state; internal PdfDocumentOpenMode _openMode; @@ -222,7 +235,7 @@ public void Save(string path) if (!CanModify) throw new InvalidOperationException(PSSR.CannotModify); - using Stream stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + using Stream stream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite/*Read access needed for SignatureHandler*/, FileShare.None); Save(stream); } @@ -308,6 +321,8 @@ public void Save(Stream stream) /// void DoSave(PdfWriter writer) { + BeforeSave(this, EventArgs.Empty); + if (_pages == null || _pages.Count == 0) { if (OutStream != null) @@ -375,6 +390,8 @@ void DoSave(PdfWriter writer) { if (writer != null) { + AfterSave(this, new PdfDocumentEventArgs(writer)); + writer.Stream.Flush(); // DO NOT CLOSE WRITER HERE //writer.Close(); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj index cf320cce..528bde2e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj @@ -34,4 +34,8 @@ + + + + From 3dd39c2986c25a906b2351d936027bdc8e5c68ad Mon Sep 17 00:00:00 2001 From: Julien R Date: Fri, 20 Oct 2023 17:22:14 +0200 Subject: [PATCH 03/20] compatibility for wpf and gdi projects --- .../src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj | 8 ++++++++ .../src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj | 8 ++++++++ .../PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs | 3 +++ .../src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs | 4 ++++ .../src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs | 3 +++ .../PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs | 6 +++++- 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj index e0431f99..9680920c 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj @@ -234,6 +234,7 @@ + @@ -296,6 +297,13 @@ + + + + + + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj index f2bab080..b09dd60d 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj @@ -233,6 +233,7 @@ + @@ -295,6 +296,13 @@ + + + + + + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs index aaaaa0ae..614a5c26 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs @@ -1,6 +1,9 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +#if WPF +using System.IO; +#endif using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs index 0d043495..eac27059 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs @@ -1,6 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +#if WPF +using System.IO; +#endif + namespace PdfSharp.Pdf.Signatures { public interface ISigner diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index d83ca483..ed433792 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -5,6 +5,9 @@ using PdfSharp.Pdf.AcroForms; using PdfSharp.Pdf.Advanced; using PdfSharp.Pdf.Annotations; +#if WPF +using System.IO; +#endif using System.Text; namespace PdfSharp.Pdf.Signatures diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs index af168a2f..84dbcbdb 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs @@ -1,9 +1,13 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +#if WPF +using System.IO; +#endif + namespace PdfSharp.Pdf.Signatures { - public class RangedStream : Stream + internal class RangedStream : Stream { private Range[] ranges; From 349dacc0270043ae418c3978575f734fe57ed817 Mon Sep 17 00:00:00 2001 From: Julien R Date: Mon, 23 Oct 2023 12:01:10 +0200 Subject: [PATCH 04/20] incorporate BouncySigner in PdfSharp + internally cache signature length --- src/Directory.Packages.props | 6 +- .../src/PdfSharp-gdi/PdfSharp-gdi.csproj | 5 ++ .../src/PdfSharp-wpf/PdfSharp-wpf.csproj | 5 ++ .../PdfSharp/Pdf.Signatures/BouncySigner.cs | 71 +++++++++++++++++++ .../PdfSharp/Pdf.Signatures/DefaultSigner.cs | 2 +- .../src/PdfSharp/Pdf.Signatures/ISigner.cs | 2 +- .../Pdf.Signatures/PdfSignatureHandler.cs | 33 ++++----- .../src/PDFsharp/src/PdfSharp/PdfSharp.csproj | 1 + 8 files changed, 103 insertions(+), 22 deletions(-) create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 3e782c0c..090575a3 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -134,7 +134,6 @@ - @@ -146,6 +145,11 @@ + + + + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj index 9680920c..3a737801 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj @@ -297,6 +297,7 @@ + @@ -421,4 +422,8 @@ + + + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj index b09dd60d..b90e27c2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj @@ -296,6 +296,7 @@ + @@ -427,4 +428,8 @@ + + + + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs new file mode 100644 index 00000000..4e186b4e --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs @@ -0,0 +1,71 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using Org.BouncyCastle.Cms; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities.Collections; +#if WPF +using System.IO; +#endif +using System.Security.Cryptography.X509Certificates; + +namespace PdfSharp.Pdf.Signatures +{ + public class BouncySigner : ISigner + { + private X509Certificate2 Certificate { get; set; } + private X509Certificate2Collection CertificateChain { get; } + + public string GetName() + { + return Certificate.GetNameInfo(X509NameType.SimpleName, false); + } + + public BouncySigner(Tuple certificateData) + { + this.Certificate = certificateData.Item1; + this.CertificateChain = certificateData.Item2; + } + + public byte[] GetSignedCms(Stream rangedStream, int pdfVersion) + { + rangedStream.Position = 0; + + CmsSignedDataGenerator signedDataGenerator = new CmsSignedDataGenerator(); + + var cert = DotNetUtilities.FromX509Certificate(Certificate); + var key = DotNetUtilities.GetKeyPair(Certificate.PrivateKey); + var allCerts = CertificateChain.OfType().Select(item => DotNetUtilities.FromX509Certificate(item)); + + var store = CollectionUtilities.CreateStore(allCerts); + + signedDataGenerator.AddSigner(key.Private, cert, GetProperDigestAlgorithm(pdfVersion)); + signedDataGenerator.AddCertificates(store); + + CmsProcessableInputStream msg = new CmsProcessableInputStream(rangedStream); + + CmsSignedData signedData = signedDataGenerator.Generate(msg, false); + + return signedData.GetEncoded(); + } + + /// + /// adbe.pkcs7.detached supported algorithms: SHA1 (PDF 1.3), SHA256 (PDF 1.6), SHA384/SHA512/RIPEMD160 (PDF 1.7) + /// + /// PDF version as int + /// + private string GetProperDigestAlgorithm(int pdfVersion) + { + switch (pdfVersion) + { + case int when pdfVersion >= 17: + return CmsSignedDataGenerator.DigestSha512; + case int when pdfVersion == 16: + return CmsSignedDataGenerator.DigestSha256; + case int when pdfVersion >= 13: + default: + return CmsSignedDataGenerator.DigestSha1; + } + } + } +} \ No newline at end of file diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs index 614a5c26..cfcb7084 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs @@ -18,7 +18,7 @@ public DefaultSigner(X509Certificate2 Certificate) this.Certificate = Certificate; } - public byte[] GetSignedCms(Stream stream, string recommendedDigestAlgorithm) + public byte[] GetSignedCms(Stream stream, int pdfVersion) { var range = new byte[stream.Length]; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs index eac27059..8f5722c2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs @@ -9,7 +9,7 @@ namespace PdfSharp.Pdf.Signatures { public interface ISigner { - byte[] GetSignedCms(Stream stream, string recommendedDigestAlgorithm); + byte[] GetSignedCms(Stream stream, int pdfVersion); string GetName(); } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index ed433792..cce92246 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -12,24 +12,22 @@ namespace PdfSharp.Pdf.Signatures { - public class IntEventArgs : EventArgs { public int Value { get; set; } } - /// /// PdfDocument signature handler. - /// Attaches a PKCS#7 signature digest to PdfDocument. PdfSharp currently supports PDF 1.4 documents, so digest algorithm should be SHA1 (supported in every subsequent version anyway): - /// adbe.pkcs7.detached supported algorithms: SHA1 (PDF 1.3), SHA256 (PDF 1.6), SHA384/SHA512/RIPEMD160 (PDF 1.7) + /// Attaches a PKCS#7 signature digest to PdfDocument. + /// Digest algorithm will be either SHA1/SHA256/SHA512 depending on PdfDocument.Version. /// public class PdfSignatureHandler { - const string SupportedDigestAlgorithm = "SHA1"; // adbe.pkcs7.detached supported algorithms: SHA1 (PDF 1.3), SHA256 (PDF 1.6), SHA384/SHA512/RIPEMD160 (PDF 1.7) - private PdfString signatureFieldContents; private PdfArray signatureFieldByteRange; - private int? maximumSignatureLength; - private const int byteRangePaddingLength = 36; // place big enough required to replace [0 0 0 0] with the correct value + /// + /// Cache signature length (bytes) for each PDF version since digest length depends on digest algorithm that depends on PDF version. + /// + private static Dictionary maximumSignatureLengthByPdfVersion = new Dictionary(); - public event EventHandler SignatureSizeComputed = (s, e) => { }; + private const int byteRangePaddingLength = 36; // place big enough required to replace [0 0 0 0] with the correct value public PdfDocument Document { get; private set; } public PdfSignatureOptions Options { get; private set; } @@ -41,17 +39,14 @@ public void AttachToDocument(PdfDocument documentToSign) this.Document.BeforeSave += AddSignatureComponents; this.Document.AfterSave += ComputeSignatureAndRange; - if (!maximumSignatureLength.HasValue) - { - maximumSignatureLength = signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), SupportedDigestAlgorithm).Length; - SignatureSizeComputed(this, new IntEventArgs() { Value = maximumSignatureLength.Value }); - } + // estimate signature length by computing signature for a fake byte[] + if (!maximumSignatureLengthByPdfVersion.ContainsKey(documentToSign.Version)) + maximumSignatureLengthByPdfVersion[documentToSign.Version] = signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), documentToSign.Version).Length; } - public PdfSignatureHandler(ISigner signer, int? signatureMaximumLength, PdfSignatureOptions options) + public PdfSignatureHandler(ISigner signer, PdfSignatureOptions options) { this.signer = signer; - this.maximumSignatureLength = signatureMaximumLength; this.Options = options; } @@ -75,8 +70,8 @@ private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) var rangeToSign = GetRangeToSign(writer.Stream); // will exclude SignatureField's /Contents from hash computation - var digest = signer.GetSignedCms(rangeToSign, SupportedDigestAlgorithm); - if (digest.Length > maximumSignatureLength) + var digest = signer.GetSignedCms(rangeToSign, Document.Version); + if (digest.Length > maximumSignatureLengthByPdfVersion[Document.Version]) throw new Exception("The digest length is bigger that the approximation made."); var hexFormatedDigest = Encoding.Default.GetBytes(FormatHex(digest)); @@ -106,7 +101,7 @@ private RangedStream GetRangeToSign(Stream stream) private void AddSignatureComponents(object sender, EventArgs e) { - var hashPlaceholderValue = new String('0', maximumSignatureLength.Value); + var hashPlaceholderValue = new String('0', maximumSignatureLengthByPdfVersion[Document.Version]); signatureFieldContents = new PdfString(hashPlaceholderValue, PdfStringFlags.HexLiteral); signatureFieldByteRange = new PdfArrayWithPadding(Document, byteRangePaddingLength, new PdfInteger(0), new PdfInteger(0), new PdfInteger(0), new PdfInteger(0)); //Document.Internals.AddObject(signatureFieldByteRange); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj index 528bde2e..c1270831 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj @@ -36,6 +36,7 @@ + From 7c055f68df08b1b21cb897f735f006b5a9c37a69 Mon Sep 17 00:00:00 2001 From: Julien R Date: Tue, 24 Oct 2023 15:23:37 +0200 Subject: [PATCH 05/20] exclude whole /Contents entry from digest instead of just its value as recommened by PDF specs. + renamings + use PdfSharp internal Hex conversion --- .../Pdf.Signatures/PdfSignatureHandler.cs | 68 ++++++++----------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index cce92246..38827354 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -5,10 +5,10 @@ using PdfSharp.Pdf.AcroForms; using PdfSharp.Pdf.Advanced; using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.Internal; #if WPF using System.IO; #endif -using System.Text; namespace PdfSharp.Pdf.Signatures { @@ -19,13 +19,13 @@ namespace PdfSharp.Pdf.Signatures /// public class PdfSignatureHandler { - private PdfString signatureFieldContents; - private PdfArray signatureFieldByteRange; + private PdfString signatureFieldContentsPdfString; + private PdfArray signatureFieldByteRangePdfArray; /// /// Cache signature length (bytes) for each PDF version since digest length depends on digest algorithm that depends on PDF version. /// - private static Dictionary maximumSignatureLengthByPdfVersion = new Dictionary(); + private static Dictionary knownSignatureLengthInBytesByPdfVersion = new Dictionary(); private const int byteRangePaddingLength = 36; // place big enough required to replace [0 0 0 0] with the correct value @@ -40,8 +40,8 @@ public void AttachToDocument(PdfDocument documentToSign) this.Document.AfterSave += ComputeSignatureAndRange; // estimate signature length by computing signature for a fake byte[] - if (!maximumSignatureLengthByPdfVersion.ContainsKey(documentToSign.Version)) - maximumSignatureLengthByPdfVersion[documentToSign.Version] = signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), documentToSign.Version).Length; + if (!knownSignatureLengthInBytesByPdfVersion.ContainsKey(documentToSign.Version)) + knownSignatureLengthInBytesByPdfVersion[documentToSign.Version] = signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), documentToSign.Version).Length; } public PdfSignatureHandler(ISigner signer, PdfSignatureOptions options) @@ -56,57 +56,47 @@ private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) // writing actual ByteRange in place of the placeholder - var rangeArray = new PdfArray(); - rangeArray.Elements.Add(new PdfInteger(0)); - rangeArray.Elements.Add(new PdfInteger(signatureFieldContents.PositionStart)); - rangeArray.Elements.Add(new PdfInteger(signatureFieldContents.PositionEnd)); - rangeArray.Elements.Add(new PdfInteger((int)writer.Stream.Length - signatureFieldContents.PositionEnd)); - - writer.Stream.Position = (signatureFieldByteRange as PdfArrayWithPadding).PositionStart; - rangeArray.WriteObject(writer); + var byteRangeArray = new PdfArray(); + byteRangeArray.Elements.Add(new PdfInteger(0)); + byteRangeArray.Elements.Add(new PdfInteger(signatureFieldContentsPdfString.PositionStart - PdfSignatureField.Keys.Contents.Length)); // PositionStart actually indicates position of the HexLiteral string right after the /Contents so we need to exclude full /Contents entry + byteRangeArray.Elements.Add(new PdfInteger(signatureFieldContentsPdfString.PositionEnd)); + byteRangeArray.Elements.Add(new PdfInteger((int)writer.Stream.Length - signatureFieldContentsPdfString.PositionEnd)); + writer.Stream.Position = (signatureFieldByteRangePdfArray as PdfArrayWithPadding).PositionStart; + byteRangeArray.WriteObject(writer); // computing and writing document's digest - var rangeToSign = GetRangeToSign(writer.Stream); // will exclude SignatureField's /Contents from hash computation - - var digest = signer.GetSignedCms(rangeToSign, Document.Version); - if (digest.Length > maximumSignatureLengthByPdfVersion[Document.Version]) - throw new Exception("The digest length is bigger that the approximation made."); - - var hexFormatedDigest = Encoding.Default.GetBytes(FormatHex(digest)); - - writer.Stream.Position = signatureFieldContents.PositionStart + 1/*' '*/ + 1/*'<'*/; // PositionStart starts right after /Contents, so we take into account the space separator and the starting '<' before writing the hash - writer.Write(hexFormatedDigest); - } - - string FormatHex(byte[] bytes) // starting from .net5, could be replaced by Convert.ToHexString(Byte[]). keeping current method to be ease .net48 compatibility - { - var retval = new StringBuilder(); + var rangedStreamToSign = GetRangeToSign(writer.Stream); // will exclude SignatureField's /Contents entry from hash computation - for (int idx = 0; idx < bytes.Length; idx++) - retval.AppendFormat("{0:x2}", bytes[idx]); + var signature = signer.GetSignedCms(rangedStreamToSign, Document.Version); + if (signature.Length != knownSignatureLengthInBytesByPdfVersion[Document.Version]) + throw new Exception("The digest length is different that the approximation made."); - return retval.ToString(); + var signatureAsRawString = PdfEncoders.RawEncoding.GetString(signature, 0, signature.Length); + var tempContentsPdfString = new PdfString(signatureAsRawString, PdfStringFlags.HexLiteral); // has to be a hex string + writer.Stream.Position = signatureFieldContentsPdfString.PositionStart + 1/*' '*/; // tempContentsPdfString is orphan, so it will not write the space. need to begin write 1 byte further + tempContentsPdfString.WriteObject(writer); } private RangedStream GetRangeToSign(Stream stream) { return new RangedStream(stream, new List() { - new RangedStream.Range(0, signatureFieldContents.PositionStart), - new RangedStream.Range(signatureFieldContents.PositionEnd, stream.Length - signatureFieldContents.PositionEnd) + new RangedStream.Range(0, signatureFieldContentsPdfString.PositionStart - PdfSignatureField.Keys.Contents.Length), // PositionStart actually indicates position of the HexLiteral string right after the /Contents so we need to exclude full /Contents entry + new RangedStream.Range(signatureFieldContentsPdfString.PositionEnd, stream.Length - signatureFieldContentsPdfString.PositionEnd) }); } private void AddSignatureComponents(object sender, EventArgs e) - { - var hashPlaceholderValue = new String('0', maximumSignatureLengthByPdfVersion[Document.Version]); - signatureFieldContents = new PdfString(hashPlaceholderValue, PdfStringFlags.HexLiteral); - signatureFieldByteRange = new PdfArrayWithPadding(Document, byteRangePaddingLength, new PdfInteger(0), new PdfInteger(0), new PdfInteger(0), new PdfInteger(0)); + { + var fakeSignature = Enumerable.Repeat((byte)0x20/*actual value does not matter*/, knownSignatureLengthInBytesByPdfVersion[Document.Version]).ToArray(); + var fakeSignatureAsRawString = PdfEncoders.RawEncoding.GetString(fakeSignature, 0, fakeSignature.Length); + signatureFieldContentsPdfString = new PdfString(fakeSignatureAsRawString, PdfStringFlags.HexLiteral); + signatureFieldByteRangePdfArray = new PdfArrayWithPadding(Document, byteRangePaddingLength, new PdfInteger(0), new PdfInteger(0), new PdfInteger(0), new PdfInteger(0)); //Document.Internals.AddObject(signatureFieldByteRange); - var signatureDictionary = GetSignatureDictionary(signatureFieldContents, signatureFieldByteRange); + var signatureDictionary = GetSignatureDictionary(signatureFieldContentsPdfString, signatureFieldByteRangePdfArray); var signatureField = GetSignatureField(signatureDictionary); RenderAppearance(signatureField, Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() { From 80cdf02d7e97dc6d1ccae3a25f949d4278188391 Mon Sep 17 00:00:00 2001 From: Julien R Date: Tue, 24 Oct 2023 17:12:54 +0200 Subject: [PATCH 06/20] properly add signature field to acroform array so that PDF readers can properly detect the signature --- .../src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs | 2 +- .../src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs index 7f56106d..8f137fb0 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs @@ -267,7 +267,7 @@ public PdfAcroFieldCollection Fields /// public sealed class PdfAcroFieldCollection : PdfArray { - PdfAcroFieldCollection(PdfArray array) + internal PdfAcroFieldCollection(PdfArray array) : base(array) { } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index 38827354..4477686e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -1,4 +1,4 @@ -// PDFsharp - A .NET library for processing PDF +// PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. using PdfSharp.Drawing; @@ -129,7 +129,7 @@ private void AddSignatureComponents(object sender, EventArgs e) } if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null) - catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, new PdfArray()); + catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, new PdfAcroField.PdfAcroFieldCollection(new PdfArray())); catalog.AcroForm.Fields.Elements.Add(signatureField); } From 15675dcee1b5b4d06e1609cfac5f6ab1af3ef231 Mon Sep 17 00:00:00 2001 From: Julien R Date: Mon, 30 Oct 2023 10:41:16 +0100 Subject: [PATCH 07/20] fix wrong position in Release mode in DEBUG, a space delimiter is added between entry key and entry value, but not in RELEASE --- .../src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index 4477686e..fd3bc653 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -75,7 +75,11 @@ private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) var signatureAsRawString = PdfEncoders.RawEncoding.GetString(signature, 0, signature.Length); var tempContentsPdfString = new PdfString(signatureAsRawString, PdfStringFlags.HexLiteral); // has to be a hex string - writer.Stream.Position = signatureFieldContentsPdfString.PositionStart + 1/*' '*/; // tempContentsPdfString is orphan, so it will not write the space. need to begin write 1 byte further + var debugAdditionalOffset = 0; +#if DEBUG + debugAdditionalOffset = 1/*' '*/; // in DEBUG mode, a space is added between entry key and entry value. tempContentsPdfString is orphan, so it will not write the space delimiter: need to begin write 1 byte further +#endif + writer.Stream.Position = signatureFieldContentsPdfString.PositionStart + debugAdditionalOffset; tempContentsPdfString.WriteObject(writer); } From 052ad9e50893e014b900d64538d78162d0b1a2fd Mon Sep 17 00:00:00 2001 From: Julien R Date: Tue, 31 Oct 2023 11:14:51 +0100 Subject: [PATCH 08/20] do not sign using obsolete SHA1 algorithm sign with SHA256 instead --- .../src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs index 4e186b4e..8d774cdb 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs @@ -1,4 +1,4 @@ -// PDFsharp - A .NET library for processing PDF +// PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. using Org.BouncyCastle.Cms; @@ -64,7 +64,7 @@ private string GetProperDigestAlgorithm(int pdfVersion) return CmsSignedDataGenerator.DigestSha256; case int when pdfVersion >= 13: default: - return CmsSignedDataGenerator.DigestSha1; + return CmsSignedDataGenerator.DigestSha256; // SHA1 is obsolete, use at least SHA256 } } } From e8f82c51ec8e51e0a3f2c3537958bb9c94e6019d Mon Sep 17 00:00:00 2001 From: Julien R Date: Tue, 31 Oct 2023 12:03:09 +0100 Subject: [PATCH 09/20] fix signature byte ranges computation --- .../Pdf.Signatures/PdfSignatureHandler.cs | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index fd3bc653..df0b8b90 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -1,4 +1,4 @@ -// PDFsharp - A .NET library for processing PDF +// PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. using PdfSharp.Drawing; @@ -15,7 +15,7 @@ namespace PdfSharp.Pdf.Signatures /// /// PdfDocument signature handler. /// Attaches a PKCS#7 signature digest to PdfDocument. - /// Digest algorithm will be either SHA1/SHA256/SHA512 depending on PdfDocument.Version. + /// Digest algorithm will be either SHA256/SHA512 depending on PdfDocument.Version. /// public class PdfSignatureHandler { @@ -52,44 +52,60 @@ public PdfSignatureHandler(ISigner signer, PdfSignatureOptions options) private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) { - var writer = e.Writer; + var writer = e.Writer; - // writing actual ByteRange in place of the placeholder + var isVerbose = writer.Layout == IO.PdfWriterLayout.Verbose; // DEBUG mode makes the writer Verbose and will introduce 1 extra space between entries key and value + // if Verbose, a space is added between entry key and entry value + var verboseExtraSpaceSeparatorLength = isVerbose ? 1 : 0; - var byteRangeArray = new PdfArray(); - byteRangeArray.Elements.Add(new PdfInteger(0)); - byteRangeArray.Elements.Add(new PdfInteger(signatureFieldContentsPdfString.PositionStart - PdfSignatureField.Keys.Contents.Length)); // PositionStart actually indicates position of the HexLiteral string right after the /Contents so we need to exclude full /Contents entry - byteRangeArray.Elements.Add(new PdfInteger(signatureFieldContentsPdfString.PositionEnd)); - byteRangeArray.Elements.Add(new PdfInteger((int)writer.Stream.Length - signatureFieldContentsPdfString.PositionEnd)); + var (rangedStreamToSign, byteRangeArray) = GetRangeToSignAndByteRangeArray(writer.Stream, verboseExtraSpaceSeparatorLength); + + // writing actual ByteRange in place of the placeholder writer.Stream.Position = (signatureFieldByteRangePdfArray as PdfArrayWithPadding).PositionStart; byteRangeArray.WriteObject(writer); // computing and writing document's digest - var rangedStreamToSign = GetRangeToSign(writer.Stream); // will exclude SignatureField's /Contents entry from hash computation - var signature = signer.GetSignedCms(rangedStreamToSign, Document.Version); + if (signature.Length != knownSignatureLengthInBytesByPdfVersion[Document.Version]) throw new Exception("The digest length is different that the approximation made."); var signatureAsRawString = PdfEncoders.RawEncoding.GetString(signature, 0, signature.Length); var tempContentsPdfString = new PdfString(signatureAsRawString, PdfStringFlags.HexLiteral); // has to be a hex string - var debugAdditionalOffset = 0; -#if DEBUG - debugAdditionalOffset = 1/*' '*/; // in DEBUG mode, a space is added between entry key and entry value. tempContentsPdfString is orphan, so it will not write the space delimiter: need to begin write 1 byte further -#endif - writer.Stream.Position = signatureFieldContentsPdfString.PositionStart + debugAdditionalOffset; + writer.Stream.Position = signatureFieldContentsPdfString.PositionStart + verboseExtraSpaceSeparatorLength; // tempContentsPdfString is orphan, so it will not write the space delimiter: need to begin write 1 byte further if Verbose tempContentsPdfString.WriteObject(writer); } - private RangedStream GetRangeToSign(Stream stream) + /// + /// Get the bytes ranges to sign. + /// As recommended in PDF specs, whole document will be signed, except for the hexadecimal signature token value in the /Contents entry. + /// Example: '/Contents ' => '' will be excluded from the bytes to sign. + /// + /// + /// + /// + private (RangedStream rangedStream, PdfArray byteRangeArray) GetRangeToSignAndByteRangeArray(Stream stream, int verboseExtraSpaceSeparatorLength) { - return new RangedStream(stream, new List() + int firstRangeOffset = 0, + firstRangeLength = signatureFieldContentsPdfString.PositionStart + verboseExtraSpaceSeparatorLength, + secondRangeOffset = signatureFieldContentsPdfString.PositionEnd, + secondRangeLength = (int)stream.Length - signatureFieldContentsPdfString.PositionEnd; + + var byteRangeArray = new PdfArray(); + byteRangeArray.Elements.Add(new PdfInteger(firstRangeOffset)); + byteRangeArray.Elements.Add(new PdfInteger(firstRangeLength)); + byteRangeArray.Elements.Add(new PdfInteger(secondRangeOffset)); + byteRangeArray.Elements.Add(new PdfInteger(secondRangeLength)); + + var rangedStream = new RangedStream(stream, new List() { - new RangedStream.Range(0, signatureFieldContentsPdfString.PositionStart - PdfSignatureField.Keys.Contents.Length), // PositionStart actually indicates position of the HexLiteral string right after the /Contents so we need to exclude full /Contents entry - new RangedStream.Range(signatureFieldContentsPdfString.PositionEnd, stream.Length - signatureFieldContentsPdfString.PositionEnd) + new RangedStream.Range(firstRangeOffset, firstRangeLength), + new RangedStream.Range(secondRangeOffset, secondRangeLength) }); + + return (rangedStream, byteRangeArray); } private void AddSignatureComponents(object sender, EventArgs e) @@ -145,7 +161,7 @@ private PdfSignatureField GetSignatureField(PdfDictionary signatureDic) // annotation keys signatureField.Elements.Add(PdfSignatureField.Keys.FT, new PdfName("/Sig")); - signatureField.Elements.Add(PdfSignatureField.Keys.T, new PdfString("Signature1")); // TODO if already exists, will it cause error? implement a name choser if yes + signatureField.Elements.Add(PdfSignatureField.Keys.T, new PdfString("Signature1")); // TODO? if already exists, will it cause error? implement a name choser if yes signatureField.Elements.Add(PdfSignatureField.Keys.Ff, new PdfInteger(132)); signatureField.Elements.Add(PdfSignatureField.Keys.DR, new PdfDictionary()); signatureField.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Annot")); From 496c5535aacfa6ac9d8db1b4dd4953fe160083bb Mon Sep 17 00:00:00 2001 From: Julien R Date: Tue, 31 Oct 2023 15:59:18 +0100 Subject: [PATCH 10/20] enable display of custom appearance for signature fields --- .../Pdf.AcroForms/PdfSignatureField.cs | 48 +++++++++++++++++++ .../Pdf.Signatures/PdfSignatureHandler.cs | 47 ++++-------------- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs index e848423c..e422f98f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs @@ -1,6 +1,9 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; + namespace PdfSharp.Pdf.AcroForms { /// @@ -19,6 +22,51 @@ internal PdfSignatureField(PdfDictionary dict) : base(dict) { } + public IAnnotationAppearanceHandler CustomAppearanceHandler { get; internal set; } + + /// + /// Creates the custom appearance form X object for the annotation that represents + /// this acro form text field. + /// + void RenderCustomAppearance() + { + PdfRectangle rect = Elements.GetRectangle(PdfAnnotation.Keys.Rect); + + var visible = !(rect.X1 + rect.X2 + rect.Y1 + rect.Y2 == 0); + + if (!visible) + return; + + if (CustomAppearanceHandler == null) + throw new Exception("AppearanceHandler is null"); + + XForm form = new XForm(_document, rect.Size); + XGraphics gfx = XGraphics.FromForm(form); + + CustomAppearanceHandler.DrawAppearance(gfx, rect.ToXRect()); + + form.DrawingFinished(); + + // Get existing or create new appearance dictionary + if (Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap) + { + ap = new PdfDictionary(_document); + Elements[PdfAnnotation.Keys.AP] = ap; + } + + // Set XRef to normal state + ap.Elements["/N"] = form.PdfForm.Reference; + + form.PdfRenderer.Close(); + } + + internal override void PrepareForSave() + { + base.PrepareForSave(); + if (CustomAppearanceHandler != null) + RenderCustomAppearance(); + } + /// /// Predefined keys of this dictionary. /// The description comes from PDF 1.4 Reference. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index df0b8b90..52b8ae1f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -117,13 +117,7 @@ private void AddSignatureComponents(object sender, EventArgs e) //Document.Internals.AddObject(signatureFieldByteRange); var signatureDictionary = GetSignatureDictionary(signatureFieldContentsPdfString, signatureFieldByteRangePdfArray); - var signatureField = GetSignatureField(signatureDictionary); - RenderAppearance(signatureField, Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() - { - Location = Options.Location, - Reason = Options.Reason, - Signer = signer.GetName() - }); + var signatureField = GetSignatureField(signatureDictionary); var annotations = Document.Pages[0].Elements.GetArray(PdfPage.Keys.Annots); if (annotations == null) @@ -170,42 +164,19 @@ private PdfSignatureField GetSignatureField(PdfDictionary signatureDic) signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle)); + signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() + { + Location = Options.Location, + Reason = Options.Reason, + Signer = signer.GetName() + }; + signatureField.PrepareForSave(); // TODO: for some reason, PdfSignatureField.PrepareForSave() is not triggered automatically so let's call it manually from here, but it would be better to be called automatically + Document.Internals.AddObject(signatureField); return signatureField; } - private void RenderAppearance(PdfSignatureField signatureField, IAnnotationAppearanceHandler appearanceHandler) - { - PdfRectangle rect = signatureField.Elements.GetRectangle(PdfAnnotation.Keys.Rect); - - var visible = !(rect.X1 + rect.X2 + rect.Y1 + rect.Y2 == 0); - - if (!visible) - return; - - if (appearanceHandler == null) - throw new Exception("AppearanceHandler is null"); - - XForm form = new XForm(Document, rect.Size); - XGraphics gfx = XGraphics.FromForm(form); - - appearanceHandler.DrawAppearance(gfx, rect.ToXRect()); - - form.DrawingFinished(); - - // Get existing or create new appearance dictionary - PdfDictionary ap = signatureField.Elements[PdfAnnotation.Keys.AP] as PdfDictionary; - if (ap == null) - { - ap = new PdfDictionary(Document); - signatureField.Elements[PdfAnnotation.Keys.AP] = ap; - } - - // Set XRef to normal state - ap.Elements["/N"] = form.PdfForm.Reference; - } - private PdfDictionary GetSignatureDictionary(PdfString contents, PdfArray byteRange) { PdfDictionary signatureDic = new PdfDictionary(Document); From 11e34ae2f6819116e7cfd553848b4dede3399c3f Mon Sep 17 00:00:00 2001 From: Julien R Date: Thu, 2 Nov 2023 18:09:10 +0100 Subject: [PATCH 11/20] get rid from obsolete X509Certificate2.PrivateKey --- .../PdfSharp/Pdf.Signatures/BouncySigner.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs index 8d774cdb..1aa06532 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs @@ -4,6 +4,7 @@ using Org.BouncyCastle.Cms; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities.Collections; +using System.Security.Cryptography; #if WPF using System.IO; #endif @@ -34,7 +35,7 @@ public byte[] GetSignedCms(Stream rangedStream, int pdfVersion) CmsSignedDataGenerator signedDataGenerator = new CmsSignedDataGenerator(); var cert = DotNetUtilities.FromX509Certificate(Certificate); - var key = DotNetUtilities.GetKeyPair(Certificate.PrivateKey); + var key = DotNetUtilities.GetKeyPair(GetAsymmetricAlgorithm(Certificate)); var allCerts = CertificateChain.OfType().Select(item => DotNetUtilities.FromX509Certificate(item)); var store = CollectionUtilities.CreateStore(allCerts); @@ -67,5 +68,20 @@ private string GetProperDigestAlgorithm(int pdfVersion) return CmsSignedDataGenerator.DigestSha256; // SHA1 is obsolete, use at least SHA256 } } + + private AsymmetricAlgorithm? GetAsymmetricAlgorithm(X509Certificate2 cert) + { + const String RSA = "1.2.840.113549.1.1.1"; + const String DSA = "1.2.840.10040.4.1"; + const String ECC = "1.2.840.10045.2.1"; + + return cert.PublicKey.Oid.Value switch + { + RSA => cert.GetRSAPrivateKey(), + DSA => cert.GetDSAPrivateKey(), + ECC => cert.GetECDsaPrivateKey(), + _ => throw new NotImplementedException(), + }; + } } } \ No newline at end of file From 8888cd1147704180574b2ecf52332162ca66eb6a Mon Sep 17 00:00:00 2001 From: Matheus Kirchesch Date: Tue, 9 Jan 2024 10:02:18 -0300 Subject: [PATCH 12/20] Added PageIndex option to the PdfSignatureOptions --- .../Pdf.Signatures/PdfSignatureHandler.cs | 19 ++++++++++++------- .../Pdf.Signatures/PdfSignatureOptions.cs | 1 + 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index 52b8ae1f..3961514f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -110,6 +110,11 @@ private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) private void AddSignatureComponents(object sender, EventArgs e) { + if (Options.PageIndex < 0 || Options.PageIndex >= Document.PageCount) + { + throw new Exception($"Signature page doesn't exist, specified page was {Options.PageIndex + 1} but document has only {Document.PageCount} page(s)."); + } + var fakeSignature = Enumerable.Repeat((byte)0x20/*actual value does not matter*/, knownSignatureLengthInBytesByPdfVersion[Document.Version]).ToArray(); var fakeSignatureAsRawString = PdfEncoders.RawEncoding.GetString(fakeSignature, 0, fakeSignature.Length); signatureFieldContentsPdfString = new PdfString(fakeSignatureAsRawString, PdfStringFlags.HexLiteral); @@ -117,11 +122,11 @@ private void AddSignatureComponents(object sender, EventArgs e) //Document.Internals.AddObject(signatureFieldByteRange); var signatureDictionary = GetSignatureDictionary(signatureFieldContentsPdfString, signatureFieldByteRangePdfArray); - var signatureField = GetSignatureField(signatureDictionary); + var signatureField = GetSignatureField(signatureDictionary); - var annotations = Document.Pages[0].Elements.GetArray(PdfPage.Keys.Annots); + var annotations = Document.Pages[Options.PageIndex].Elements.GetArray(PdfPage.Keys.Annots); if (annotations == null) - Document.Pages[0].Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField)); + Document.Pages[Options.PageIndex].Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField)); else annotations.Elements.Add(signatureField); @@ -129,7 +134,7 @@ private void AddSignatureComponents(object sender, EventArgs e) // acroform var catalog = Document.Catalog; - + if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); @@ -160,8 +165,8 @@ private PdfSignatureField GetSignatureField(PdfDictionary signatureDic) signatureField.Elements.Add(PdfSignatureField.Keys.DR, new PdfDictionary()); signatureField.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Annot")); signatureField.Elements.Add("/Subtype", new PdfName("/Widget")); - signatureField.Elements.Add("/P", Document.Pages[0]); - + signatureField.Elements.Add("/P", Document.Pages[Options.PageIndex]); + signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle)); signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() @@ -194,6 +199,6 @@ private PdfDictionary GetSignatureDictionary(PdfString contents, PdfArray byteRa Document.Internals.AddObject(signatureDic); return signatureDic; - } + } } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs index ae285b7f..3fab7924 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs @@ -13,5 +13,6 @@ public class PdfSignatureOptions public string Location { get; set; } public string Reason { get; set; } public XRect Rectangle { get; set; } + public int PageIndex { get; set; } } } From 13119693af3e67b98e215ca496ed06a164664130 Mon Sep 17 00:00:00 2001 From: Julien R Date: Tue, 9 Jan 2024 16:05:18 +0100 Subject: [PATCH 13/20] signature handler: check nullity of arguments + comments --- .../Pdf.Signatures/PdfSignatureHandler.cs | 25 +++++++++++++++---- .../Pdf.Signatures/PdfSignatureOptions.cs | 3 +++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index 3961514f..59a202a5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -29,10 +29,21 @@ public class PdfSignatureHandler private const int byteRangePaddingLength = 36; // place big enough required to replace [0 0 0 0] with the correct value + /// + /// Pdf Document signature will be attached to + /// public PdfDocument Document { get; private set; } + + /// + /// Signature options + /// public PdfSignatureOptions Options { get; private set; } private ISigner signer { get; set; } + /// + /// Attach this signature handler to the given Pdf document + /// + /// Pdf document to sign public void AttachToDocument(PdfDocument documentToSign) { this.Document = documentToSign; @@ -46,6 +57,12 @@ public void AttachToDocument(PdfDocument documentToSign) public PdfSignatureHandler(ISigner signer, PdfSignatureOptions options) { + ArgumentNullException.ThrowIfNull(signer); + ArgumentNullException.ThrowIfNull(options); + + if (options.PageIndex < 0) + throw new ArgumentOutOfRangeException($"Signature page index cannot be negative."); + this.signer = signer; this.Options = options; } @@ -81,7 +98,7 @@ private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) /// /// Get the bytes ranges to sign. /// As recommended in PDF specs, whole document will be signed, except for the hexadecimal signature token value in the /Contents entry. - /// Example: '/Contents ' => '' will be excluded from the bytes to sign. + /// Example: '/Contents <aaaaa111111>' => '<aaaaa111111>' will be excluded from the bytes to sign. /// /// /// @@ -110,10 +127,8 @@ private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) private void AddSignatureComponents(object sender, EventArgs e) { - if (Options.PageIndex < 0 || Options.PageIndex >= Document.PageCount) - { - throw new Exception($"Signature page doesn't exist, specified page was {Options.PageIndex + 1} but document has only {Document.PageCount} page(s)."); - } + if (Options.PageIndex >= Document.PageCount) + throw new ArgumentOutOfRangeException($"Signature page doesn't exist, specified page was {Options.PageIndex + 1} but document has only {Document.PageCount} page(s)."); var fakeSignature = Enumerable.Repeat((byte)0x20/*actual value does not matter*/, knownSignatureLengthInBytesByPdfVersion[Document.Version]).ToArray(); var fakeSignatureAsRawString = PdfEncoders.RawEncoding.GetString(fakeSignature, 0, fakeSignature.Length); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs index 3fab7924..1867ff54 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs @@ -13,6 +13,9 @@ public class PdfSignatureOptions public string Location { get; set; } public string Reason { get; set; } public XRect Rectangle { get; set; } + /// + /// page index, zero-based + /// public int PageIndex { get; set; } } } From 55ddebb7e38d284d788e4062804e3bdcd95fc94c Mon Sep 17 00:00:00 2001 From: Julien R Date: Fri, 23 Feb 2024 17:05:36 +0100 Subject: [PATCH 14/20] DefaultSigner: add the ability to attach a signature timestamp from TSA --- .../PdfSharp/Pdf.Signatures/DefaultSigner.cs | 66 +++++++++++++++++-- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs index cfcb7084..abc72690 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs @@ -4,6 +4,8 @@ #if WPF using System.IO; #endif +using System.Net.Http.Headers; +using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; @@ -11,27 +13,36 @@ namespace PdfSharp.Pdf.Signatures { public class DefaultSigner : ISigner { - public X509Certificate2 Certificate { get; private set; } + private static readonly Oid SignatureTimeStampOin = new Oid("1.2.840.113549.1.9.16.2.14"); + private static readonly string TimestampQueryContentType = "application/timestamp-query"; + private static readonly string TimestampReplyContentType = "application/timestamp-reply"; - public DefaultSigner(X509Certificate2 Certificate) + private X509Certificate2 _certificate { get; init; } + private Uri? _timeStampAuthorityUri { get; init; } + + public DefaultSigner(X509Certificate2 Certificate, Uri? timeStampAuthorityUri = null) { - this.Certificate = Certificate; + _certificate = Certificate; + _timeStampAuthorityUri = timeStampAuthorityUri; } public byte[] GetSignedCms(Stream stream, int pdfVersion) { var range = new byte[stream.Length]; - stream.Position = 0; stream.Read(range, 0, range.Length); + // Sign the byte range var contentInfo = new ContentInfo(range); - SignedCms signedCms = new SignedCms(contentInfo, true); - CmsSigner signer = new CmsSigner(Certificate); + CmsSigner signer = new CmsSigner(_certificate)/* { IncludeOption = X509IncludeOption.WholeChain }*/; signer.UnsignedAttributes.Add(new Pkcs9SigningTime()); signedCms.ComputeSignature(signer, true); + + if (_timeStampAuthorityUri is not null) + Task.Run(() => AddTimestampFromTSAAsync(signedCms)).Wait(); + var bytes = signedCms.Encode(); return bytes; @@ -39,7 +50,48 @@ public byte[] GetSignedCms(Stream stream, int pdfVersion) public string GetName() { - return Certificate.GetNameInfo(X509NameType.SimpleName, false); + return _certificate.GetNameInfo(X509NameType.SimpleName, false); + } + + private async Task AddTimestampFromTSAAsync(SignedCms signedCms) + { + // Generate our nonce to identify the pair request-response + byte[] nonce = new byte[8]; +#if NET6_0_OR_GREATER + nonce = RandomNumberGenerator.GetBytes(8); +#else + using var cryptoProvider = new RNGCryptoServiceProvider(); + cryptoProvider.GetBytes(nonce = new Byte[8]); +#endif + // Get our signing information and create the RFC3161 request + SignerInfo newSignerInfo = signedCms.SignerInfos[0]; + // Now we generate our request for us to send to our RFC3161 signing authority. + var request = Rfc3161TimestampRequest.CreateFromSignerInfo( + newSignerInfo, + HashAlgorithmName.SHA256, + requestSignerCertificates: true, // ask TSA to embed its signing certificate in the timestamp token + nonce: nonce); + + var client = new HttpClient(); + var content = new ReadOnlyMemoryContent(request.Encode()); + content.Headers.ContentType = new MediaTypeHeaderValue(TimestampQueryContentType); + var httpResponse = await client.PostAsync(_timeStampAuthorityUri, content).ConfigureAwait(false); + + // Process our response + if (!httpResponse.IsSuccessStatusCode) + { + throw new CryptographicException( + $"There was a error from the timestamp authority. It responded with {httpResponse.StatusCode} {(int)httpResponse.StatusCode}: {httpResponse.Content}"); + } + if (httpResponse.Content.Headers.ContentType.MediaType != TimestampReplyContentType) + { + throw new CryptographicException("The reply from the time stamp server was in a invalid format."); + } + var data = await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + var timestampToken = request.ProcessResponse(data, out _); + + // The RFC3161 sign certificate is separate to the contents that was signed, we need to add it to the unsigned attributes. + newSignerInfo.AddUnsignedAttribute(new AsnEncodedData(SignatureTimeStampOin, timestampToken.AsSignedCms().Encode())); } } } From 8a9034967750405ff39c18caca5505bbb8b53c17 Mon Sep 17 00:00:00 2001 From: Julien R Date: Mon, 26 Feb 2024 11:37:38 +0100 Subject: [PATCH 15/20] fix issue of varying TSA answer size by adding a 10 byte margin value 10 is arbitrary, I hope this is enough for all scenarios. trailing zeros have no incidence on signature decoding so this is OK to have trailing zeros. --- .../Pdf.Signatures/PdfSignatureHandler.cs | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index 59a202a5..5c1c56a5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -1,11 +1,10 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -using PdfSharp.Drawing; using PdfSharp.Pdf.AcroForms; using PdfSharp.Pdf.Advanced; -using PdfSharp.Pdf.Annotations; using PdfSharp.Pdf.Internal; +using System.Text; #if WPF using System.IO; #endif @@ -23,11 +22,14 @@ public class PdfSignatureHandler private PdfArray signatureFieldByteRangePdfArray; /// - /// Cache signature length (bytes) for each PDF version since digest length depends on digest algorithm that depends on PDF version. + /// Cached signature length (in bytes) for each PDF version since digest length depends on digest algorithm that depends on PDF version. /// - private static Dictionary knownSignatureLengthInBytesByPdfVersion = new Dictionary(); + private static Dictionary knownSignatureLengthInBytesByPdfVersion = new(); - private const int byteRangePaddingLength = 36; // place big enough required to replace [0 0 0 0] with the correct value + /// + /// (arbitrary) big enough reserved space to replace ByteRange placeholder [0 0 0 0] with the actual computed value of the byte range to sign + /// + private const int byteRangePaddingLength = 36; /// /// Pdf Document signature will be attached to @@ -52,7 +54,9 @@ public void AttachToDocument(PdfDocument documentToSign) // estimate signature length by computing signature for a fake byte[] if (!knownSignatureLengthInBytesByPdfVersion.ContainsKey(documentToSign.Version)) - knownSignatureLengthInBytesByPdfVersion[documentToSign.Version] = signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), documentToSign.Version).Length; + knownSignatureLengthInBytesByPdfVersion[documentToSign.Version] = + signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), documentToSign.Version).Length + + 10 /* arbitrary margin added because TSA timestamp response's length seems to vary from a call to another (I saw a variation of 1 byte) */; } public PdfSignatureHandler(ISigner signer, PdfSignatureOptions options) @@ -82,17 +86,27 @@ private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) writer.Stream.Position = (signatureFieldByteRangePdfArray as PdfArrayWithPadding).PositionStart; byteRangeArray.WriteObject(writer); - // computing and writing document's digest - + // computing signature from document's digest var signature = signer.GetSignedCms(rangedStreamToSign, Document.Version); - if (signature.Length != knownSignatureLengthInBytesByPdfVersion[Document.Version]) - throw new Exception("The digest length is different that the approximation made."); + if (signature.Length > knownSignatureLengthInBytesByPdfVersion[Document.Version]) + throw new Exception("The actual digest length is bigger that the approximation made. Not enough room in the placeholder to fit the signature."); + + // directly writes document's signature in the /Contents<> entry + writer.Stream.Position = signatureFieldContentsPdfString.PositionStart + + verboseExtraSpaceSeparatorLength /* tempContentsPdfString is orphan, so it will not write the space delimiter: need to begin write 1 byte further if Verbose */ + + 1 /* skip the begin-delimiter '<' */; + writer.Write(PdfEncoders.RawEncoding.GetBytes(FormatHex(signature))); + } + + private string FormatHex(byte[] bytes) // starting from .net5, could be replaced by Convert.ToHexString(Byte[]). keeping current method to be ease .net48/netstandard compatibility + { + var retval = new StringBuilder(); + + for (int idx = 0; idx < bytes.Length; idx++) + retval.AppendFormat("{0:X2}", bytes[idx]); - var signatureAsRawString = PdfEncoders.RawEncoding.GetString(signature, 0, signature.Length); - var tempContentsPdfString = new PdfString(signatureAsRawString, PdfStringFlags.HexLiteral); // has to be a hex string - writer.Stream.Position = signatureFieldContentsPdfString.PositionStart + verboseExtraSpaceSeparatorLength; // tempContentsPdfString is orphan, so it will not write the space delimiter: need to begin write 1 byte further if Verbose - tempContentsPdfString.WriteObject(writer); + return retval.ToString(); } /// @@ -130,9 +144,9 @@ private void AddSignatureComponents(object sender, EventArgs e) if (Options.PageIndex >= Document.PageCount) throw new ArgumentOutOfRangeException($"Signature page doesn't exist, specified page was {Options.PageIndex + 1} but document has only {Document.PageCount} page(s)."); - var fakeSignature = Enumerable.Repeat((byte)0x20/*actual value does not matter*/, knownSignatureLengthInBytesByPdfVersion[Document.Version]).ToArray(); + var fakeSignature = Enumerable.Repeat((byte)0x00/*padded with zeros, as recommended (trailing zeros have no incidence on signature decoding)*/, knownSignatureLengthInBytesByPdfVersion[Document.Version]).ToArray(); var fakeSignatureAsRawString = PdfEncoders.RawEncoding.GetString(fakeSignature, 0, fakeSignature.Length); - signatureFieldContentsPdfString = new PdfString(fakeSignatureAsRawString, PdfStringFlags.HexLiteral); + signatureFieldContentsPdfString = new PdfString(fakeSignatureAsRawString, PdfStringFlags.HexLiteral); // has to be a hex string signatureFieldByteRangePdfArray = new PdfArrayWithPadding(Document, byteRangePaddingLength, new PdfInteger(0), new PdfInteger(0), new PdfInteger(0), new PdfInteger(0)); //Document.Internals.AddObject(signatureFieldByteRange); From 712ef3a3ccdd5a9f76eeb943a437410388b2bbaa Mon Sep 17 00:00:00 2001 From: Julien R Date: Fri, 7 Jun 2024 15:16:48 +0200 Subject: [PATCH 16/20] compatibility of ArgumentNullException.ThrowIfNull --- .../src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index 5c1c56a5..3623d6a2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -61,8 +61,10 @@ public void AttachToDocument(PdfDocument documentToSign) public PdfSignatureHandler(ISigner signer, PdfSignatureOptions options) { - ArgumentNullException.ThrowIfNull(signer); - ArgumentNullException.ThrowIfNull(options); + if (signer is null) + throw new ArgumentNullException(nameof(signer)); + if (options is null) + throw new ArgumentNullException(nameof(options)); if (options.PageIndex < 0) throw new ArgumentOutOfRangeException($"Signature page index cannot be negative."); From 7d5c01b0d5efe692508bc68b20264c6590806acd Mon Sep 17 00:00:00 2001 From: Julien R Date: Fri, 7 Jun 2024 16:15:41 +0200 Subject: [PATCH 17/20] DSA not supported in netstandard2.0 --- .../src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs index 1aa06532..64a6b28a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs @@ -78,7 +78,9 @@ private string GetProperDigestAlgorithm(int pdfVersion) return cert.PublicKey.Oid.Value switch { RSA => cert.GetRSAPrivateKey(), +#if NET6_0_OR_GREATER DSA => cert.GetDSAPrivateKey(), +#endif ECC => cert.GetECDsaPrivateKey(), _ => throw new NotImplementedException(), }; From 68d42bb1a1abb75e716b7fb3edb734cf9705b57e Mon Sep 17 00:00:00 2001 From: Julien R Date: Fri, 7 Jun 2024 16:33:52 +0200 Subject: [PATCH 18/20] use long instead of int for Stream Position --- .../PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs | 4 ++-- .../PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs | 12 ++++++------ .../src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs index 9a962279..5bbb8461 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs @@ -36,11 +36,11 @@ internal override void WriteObject(PdfWriter writer) /// /// Position of the first byte of this string in PdfWriter's Stream /// - public int PositionStart { get; internal set; } + public long PositionStart { get; internal set; } /// /// Position of the last byte of this string in PdfWriter's Stream /// - public int PositionEnd { get; internal set; } + public long PositionEnd { get; internal set; } } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index 3623d6a2..bd14869a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -121,16 +121,16 @@ private string FormatHex(byte[] bytes) // starting from .net5, could be replaced /// private (RangedStream rangedStream, PdfArray byteRangeArray) GetRangeToSignAndByteRangeArray(Stream stream, int verboseExtraSpaceSeparatorLength) { - int firstRangeOffset = 0, + long firstRangeOffset = 0, firstRangeLength = signatureFieldContentsPdfString.PositionStart + verboseExtraSpaceSeparatorLength, secondRangeOffset = signatureFieldContentsPdfString.PositionEnd, secondRangeLength = (int)stream.Length - signatureFieldContentsPdfString.PositionEnd; var byteRangeArray = new PdfArray(); - byteRangeArray.Elements.Add(new PdfInteger(firstRangeOffset)); - byteRangeArray.Elements.Add(new PdfInteger(firstRangeLength)); - byteRangeArray.Elements.Add(new PdfInteger(secondRangeOffset)); - byteRangeArray.Elements.Add(new PdfInteger(secondRangeLength)); + byteRangeArray.Elements.Add(new PdfLongInteger(firstRangeOffset)); + byteRangeArray.Elements.Add(new PdfLongInteger(firstRangeLength)); + byteRangeArray.Elements.Add(new PdfLongInteger(secondRangeOffset)); + byteRangeArray.Elements.Add(new PdfLongInteger(secondRangeLength)); var rangedStream = new RangedStream(stream, new List() { @@ -149,7 +149,7 @@ private void AddSignatureComponents(object sender, EventArgs e) var fakeSignature = Enumerable.Repeat((byte)0x00/*padded with zeros, as recommended (trailing zeros have no incidence on signature decoding)*/, knownSignatureLengthInBytesByPdfVersion[Document.Version]).ToArray(); var fakeSignatureAsRawString = PdfEncoders.RawEncoding.GetString(fakeSignature, 0, fakeSignature.Length); signatureFieldContentsPdfString = new PdfString(fakeSignatureAsRawString, PdfStringFlags.HexLiteral); // has to be a hex string - signatureFieldByteRangePdfArray = new PdfArrayWithPadding(Document, byteRangePaddingLength, new PdfInteger(0), new PdfInteger(0), new PdfInteger(0), new PdfInteger(0)); + signatureFieldByteRangePdfArray = new PdfArrayWithPadding(Document, byteRangePaddingLength, new PdfLongInteger(0), new PdfLongInteger(0), new PdfLongInteger(0), new PdfLongInteger(0)); //Document.Internals.AddObject(signatureFieldByteRange); var signatureDictionary = GetSignatureDictionary(signatureFieldContentsPdfString, signatureFieldByteRangePdfArray); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs index 03aaa48d..75c924a2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs @@ -430,11 +430,11 @@ internal override void WriteObject(PdfWriter writer) /// /// Position of the first byte of this string in PdfWriter's Stream /// - public int PositionStart { get; internal set; } + public long PositionStart { get; internal set; } /// /// Position of the last byte of this string in PdfWriter's Stream /// - public int PositionEnd { get; internal set; } + public long PositionEnd { get; internal set; } } } From eb1e4bb78f990b36c2bad57f4c52de967e525743 Mon Sep 17 00:00:00 2001 From: Julien R Date: Tue, 11 Jun 2024 11:28:54 +0200 Subject: [PATCH 19/20] fix build (making signatures timestamp feature only available on net6+) --- src/Directory.Packages.props | 2 +- .../src/PdfSharp-gdi/PdfSharp-gdi.csproj | 3 ++- .../src/PdfSharp-wpf/PdfSharp-wpf.csproj | 3 ++- .../PdfSharp/Pdf.Signatures/DefaultSigner.cs | 23 +++++++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index ae1e18e3..c7ec271d 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -100,7 +100,7 @@ - + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj index 70c8791b..4b1c2957 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj @@ -1,4 +1,4 @@ - + library @@ -434,6 +434,7 @@ + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj index 5c2875e5..32f1461e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj @@ -1,4 +1,4 @@ - + library @@ -440,6 +440,7 @@ --> + diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs index abc72690..db8c8563 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs @@ -4,7 +4,10 @@ #if WPF using System.IO; #endif +#if NET6_0_OR_GREATER +using System.Net.Http; using System.Net.Http.Headers; +#endif using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; @@ -20,11 +23,23 @@ public class DefaultSigner : ISigner private X509Certificate2 _certificate { get; init; } private Uri? _timeStampAuthorityUri { get; init; } + public DefaultSigner(X509Certificate2 Certificate) + { + _certificate = Certificate; + } + +#if NET6_0_OR_GREATER + /// + /// using a TimeStamp Authority to add timestamp to signature, only on net6+ for now due to available classes for Rfc3161 + /// + /// + /// public DefaultSigner(X509Certificate2 Certificate, Uri? timeStampAuthorityUri = null) { _certificate = Certificate; _timeStampAuthorityUri = timeStampAuthorityUri; } +#endif public byte[] GetSignedCms(Stream stream, int pdfVersion) { @@ -40,8 +55,10 @@ public byte[] GetSignedCms(Stream stream, int pdfVersion) signedCms.ComputeSignature(signer, true); +#if NET6_0_OR_GREATER if (_timeStampAuthorityUri is not null) Task.Run(() => AddTimestampFromTSAAsync(signedCms)).Wait(); +#endif var bytes = signedCms.Encode(); @@ -53,6 +70,7 @@ public string GetName() return _certificate.GetNameInfo(X509NameType.SimpleName, false); } +#if NET6_0_OR_GREATER private async Task AddTimestampFromTSAAsync(SignedCms signedCms) { // Generate our nonce to identify the pair request-response @@ -91,7 +109,12 @@ private async Task AddTimestampFromTSAAsync(SignedCms signedCms) var timestampToken = request.ProcessResponse(data, out _); // The RFC3161 sign certificate is separate to the contents that was signed, we need to add it to the unsigned attributes. +#if NET6_0_OR_GREATER newSignerInfo.AddUnsignedAttribute(new AsnEncodedData(SignatureTimeStampOin, timestampToken.AsSignedCms().Encode())); +#else + newSignerInfo.UnsignedAttributes.Add(new AsnEncodedData(SignatureTimeStampOin, timestampToken.AsSignedCms().Encode())); +#endif } +#endif } } From 22352a113ab57d286407e375de0adac547d00a50 Mon Sep 17 00:00:00 2001 From: Julien R Date: Tue, 11 Jun 2024 14:29:51 +0200 Subject: [PATCH 20/20] update BouncyCastle.Cryptography (vulnerability warning) --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index c7ec271d..6777d2f0 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -101,7 +101,7 @@ - +