diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index fe9f2272..6777d2f0 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,113 +1,116 @@ - - - - 6.0.0 - 6.0.0 - 6.0.0 - 6.0.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + 6.0.0 + 6.0.0 + 6.0.0 + 6.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 51dc232e..4b1c2957 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj @@ -256,6 +256,7 @@ + @@ -319,6 +320,14 @@ + + + + + + + + @@ -424,4 +433,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 e32b4515..32f1461e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj @@ -255,6 +255,7 @@ + @@ -318,6 +319,14 @@ + + + + + + + + @@ -430,4 +439,9 @@ --> + + + + + 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 79afe41b..17089acd 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.AcroForms/PdfSignatureField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs index d1f84691..429a6f9f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs @@ -1,7 +1,9 @@ -// 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.Pdf.IO; +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; namespace PdfSharp.Pdf.AcroForms { @@ -21,6 +23,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(); + } + /// /// Writes a key/value pair of this signature field dictionary. /// 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/BouncySigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs new file mode 100644 index 00000000..64a6b28a --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs @@ -0,0 +1,89 @@ +// 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; +using System.Security.Cryptography; +#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(GetAsymmetricAlgorithm(Certificate)); + 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.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(), +#if NET6_0_OR_GREATER + DSA => cert.GetDSAPrivateKey(), +#endif + ECC => cert.GetECDsaPrivateKey(), + _ => throw new NotImplementedException(), + }; + } + } +} \ No newline at end of file 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..db8c8563 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs @@ -0,0 +1,120 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +#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; + +namespace PdfSharp.Pdf.Signatures +{ + public class DefaultSigner : ISigner + { + 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"; + + 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) + { + 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)/* { IncludeOption = X509IncludeOption.WholeChain }*/; + signer.UnsignedAttributes.Add(new Pkcs9SigningTime()); + + signedCms.ComputeSignature(signer, true); + +#if NET6_0_OR_GREATER + if (_timeStampAuthorityUri is not null) + Task.Run(() => AddTimestampFromTSAAsync(signedCms)).Wait(); +#endif + + var bytes = signedCms.Encode(); + + return bytes; + } + + 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 + 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. +#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 + } +} 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..8f5722c2 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs @@ -0,0 +1,16 @@ +// 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 + { + byte[] GetSignedCms(Stream stream, int pdfVersion); + + 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..5bbb8461 --- /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 long PositionStart { get; internal set; } + + /// + /// Position of the last byte of this string in PdfWriter's Stream + /// + 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 new file mode 100644 index 00000000..bd14869a --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -0,0 +1,235 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using PdfSharp.Pdf.AcroForms; +using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Internal; +using System.Text; +#if WPF +using System.IO; +#endif + +namespace PdfSharp.Pdf.Signatures +{ + /// + /// PdfDocument signature handler. + /// Attaches a PKCS#7 signature digest to PdfDocument. + /// Digest algorithm will be either SHA256/SHA512 depending on PdfDocument.Version. + /// + public class PdfSignatureHandler + { + private PdfString signatureFieldContentsPdfString; + private PdfArray signatureFieldByteRangePdfArray; + + /// + /// 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(); + + /// + /// (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 + /// + 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; + this.Document.BeforeSave += AddSignatureComponents; + this.Document.AfterSave += ComputeSignatureAndRange; + + // 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 + + 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) + { + 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."); + + this.signer = signer; + this.Options = options; + } + + private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e) + { + var writer = e.Writer; + + 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 (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 signature from document's digest + var signature = signer.GetSignedCms(rangedStreamToSign, Document.Version); + + 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]); + + return retval.ToString(); + } + + /// + /// 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 <aaaaa111111>' => '<aaaaa111111>' will be excluded from the bytes to sign. + /// + /// + /// + /// + private (RangedStream rangedStream, PdfArray byteRangeArray) GetRangeToSignAndByteRangeArray(Stream stream, int verboseExtraSpaceSeparatorLength) + { + long firstRangeOffset = 0, + firstRangeLength = signatureFieldContentsPdfString.PositionStart + verboseExtraSpaceSeparatorLength, + secondRangeOffset = signatureFieldContentsPdfString.PositionEnd, + secondRangeLength = (int)stream.Length - signatureFieldContentsPdfString.PositionEnd; + + var byteRangeArray = new PdfArray(); + 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() + { + new RangedStream.Range(firstRangeOffset, firstRangeLength), + new RangedStream.Range(secondRangeOffset, secondRangeLength) + }); + + return (rangedStream, byteRangeArray); + } + + 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)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 PdfLongInteger(0), new PdfLongInteger(0), new PdfLongInteger(0), new PdfLongInteger(0)); + //Document.Internals.AddObject(signatureFieldByteRange); + + var signatureDictionary = GetSignatureDictionary(signatureFieldContentsPdfString, signatureFieldByteRangePdfArray); + var signatureField = GetSignatureField(signatureDictionary); + + var annotations = Document.Pages[Options.PageIndex].Elements.GetArray(PdfPage.Keys.Annots); + if (annotations == null) + Document.Pages[Options.PageIndex].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 PdfAcroField.PdfAcroFieldCollection(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[Options.PageIndex]); + + 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 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..1867ff54 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs @@ -0,0 +1,21 @@ +// 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; } + /// + /// page index, zero-based + /// + public int PageIndex { 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..84dbcbdb --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs @@ -0,0 +1,168 @@ +// 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 +{ + internal 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 9b3ad75d..5bf645bf 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.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 System.Runtime.InteropServices; @@ -19,12 +19,24 @@ 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) => { }; #if DEBUG_ static PdfDocument() { @@ -220,7 +232,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); } @@ -312,6 +324,8 @@ void DoSave(PdfWriter writer) { PdfSharpLogHost.Logger.PdfDocumentSaved(Name); + BeforeSave(this, EventArgs.Empty); + if (_pages == null || _pages.Count == 0) { if (OutStream != null) @@ -376,6 +390,8 @@ void DoSave(PdfWriter writer) { if (writer != null!) { + AfterSave(this, new PdfDocumentEventArgs(writer)); + writer.Stream.Flush(); // DO NOT CLOSE WRITER HERE } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs index 9d83a5ce..75c924a2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs @@ -420,7 +420,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 long PositionStart { get; internal set; } + + /// + /// Position of the last byte of this string in PdfWriter's Stream + /// + public long PositionEnd { get; internal set; } } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj index cefdd52c..217bb259 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj @@ -58,4 +58,9 @@ + + + + +