From acd78bf78694a5d1597460372e6bc22ccd4dec00 Mon Sep 17 00:00:00 2001 From: Thomas Terhaar Date: Sat, 15 Jun 2024 12:43:56 +0200 Subject: [PATCH] POC: Document signing based on #48 with support for multiple signatures --- src/Directory.Packages.props | 4 + .../PdfSharp/Pdf.AcroForms/PdfAcroField.cs | 8 + .../Pdf.AcroForms/PdfSignatureField.cs | 37 +- .../src/PdfSharp/Pdf.Advanced/PdfCatalog.cs | 21 +- .../Pdf.Advanced/PdfCrossReferenceTable.cs | 2 +- .../PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs | 6 +- .../DefaultSignatureRenderer.cs | 48 +++ .../PdfSharp/Pdf.Signatures/DefaultSigner.cs | 105 +++++ .../Pdf.Signatures/ISignatureRenderer.cs | 9 + .../src/PdfSharp/Pdf.Signatures/ISigner.cs | 11 + .../Pdf.Signatures/PdfSignatureOptions.cs | 69 +++ .../Pdf.Signatures/PdfSignatureValue.cs | 399 ++++++++++++++++++ .../src/PdfSharp/Pdf.Signatures/PdfSigner.cs | 256 +++++++++++ .../PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 44 +- .../src/PDFsharp/src/PdfSharp/PdfSharp.csproj | 4 + .../tests/PdfSharp.Tests/IO/WriterTests.cs | 35 ++ 16 files changed, 1037 insertions(+), 21 deletions(-) create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureRenderer.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/ISignatureRenderer.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/PdfSignatureOptions.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureValue.cs create mode 100644 src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSigner.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index fe9f2272..3a1c2b0e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -89,6 +89,7 @@ + @@ -99,6 +100,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..bc425422 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs @@ -34,6 +34,10 @@ public string Name string name = Elements.GetString(Keys.T); return name; } + set + { + Elements.SetString(Keys.T, value); + } } /// @@ -267,6 +271,10 @@ public PdfAcroFieldCollection Fields /// public sealed class PdfAcroFieldCollection : PdfArray { + PdfAcroFieldCollection(PdfDocument document) + : base(document) + { } + 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..4f15fe08 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs @@ -2,6 +2,7 @@ // See the LICENSE file in the solution root for more information. using PdfSharp.Pdf.IO; +using PdfSharp.Pdf.Signatures; namespace PdfSharp.Pdf.AcroForms { @@ -15,32 +16,44 @@ public sealed class PdfSignatureField : PdfAcroField /// internal PdfSignatureField(PdfDocument document) : base(document) - { } + { + Elements[PdfAcroField.Keys.FT] = new PdfName("/Sig"); + } internal PdfSignatureField(PdfDictionary dict) : base(dict) { } /// - /// Writes a key/value pair of this signature field dictionary. + /// Gets or sets the value for this field /// - internal override void WriteDictionaryElement(PdfWriter writer, PdfName key) + public new PdfSignatureValue? Value { - // Don’t encrypt Contents key’s value (PDF Reference 2.0: 7.6.2, Page 71). - if (key.Value == Keys.Contents) + get + { + if (sigValue is null) + { + var dict = Elements.GetValue(PdfAcroField.Keys.V) as PdfDictionary; + if (dict is not null) + sigValue = new PdfSignatureValue(dict); + } + return sigValue; + } + set { - var effectiveSecurityHandler = writer.EffectiveSecurityHandler; - writer.EffectiveSecurityHandler = null; - base.WriteDictionaryElement(writer, key); - writer.EffectiveSecurityHandler = effectiveSecurityHandler; + if (value is not null) + Elements.SetReference(PdfAcroField.Keys.V, value); + else + Elements.Remove(PdfAcroField.Keys.V); } - else - base.WriteDictionaryElement(writer, key); } + PdfSignatureValue? sigValue; /// /// Predefined keys of this dictionary. - /// The description comes from PDF 1.4 Reference. + /// The description comes from PDF 1.4 Reference.

+ /// TODO: These are wrong ! + /// The keys are for a , not for a ///
public new class Keys : PdfAcroField.Keys { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs index a20bdeb2..2eaa522d 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs @@ -152,14 +152,31 @@ public PdfNameDictionary Names /// /// Gets the AcroForm dictionary of this document. /// - public PdfAcroForm AcroForm + public PdfAcroForm? AcroForm { get { if (_acroForm == null) - _acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm)??NRT.ThrowOnNull(); + _acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm); return _acroForm; } + internal set + { + if (value != null) + { + if (!value.IsIndirect) + throw new InvalidOperationException("Setting the AcroForm requires an indirect object"); + Elements.SetReference(Keys.AcroForm, value); + _acroForm = value; + } + else + { + if (AcroForm != null && AcroForm.Reference != null) + _document.IrefTable.Remove(AcroForm.Reference); + Elements.Remove(Keys.AcroForm); + _acroForm = null; + } + } } PdfAcroForm? _acroForm; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs index 9974a501..b2335566 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs @@ -118,7 +118,7 @@ public void Add(PdfObject value) ObjectTable.Add(value.ObjectID, value.ReferenceNotNull); if (ReadyForModification && _document.IsAppending) - ModifiedObjects[value.ObjectID] = value.Reference; + ModifiedObjects[value.ObjectID] = value.ReferenceNotNull; } /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs index c81d5802..ad906e90 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs @@ -450,7 +450,7 @@ void FinishReferences() "All references saved in IrefTable should have been created when their referred PdfObject has been accessible."); // Get and update object’s references. - FinishItemReferences(iref.Value, _document, finishedObjects); + FinishItemReferences(iref.Value, iref, _document, finishedObjects); } // why setting it here AND in Trailer.Finish ?? @@ -526,7 +526,7 @@ void FinishChildReferences(PdfDictionary dictionary, PdfReference containingRefe } // Get and update item’s references. - FinishItemReferences(item, _document, finishedObjects); + FinishItemReferences(item, containingReference, _document, finishedObjects); } } @@ -549,7 +549,7 @@ void FinishChildReferences(PdfArray array, PdfReference containingReference, Has } // Get and update item’s references. - FinishItemReferences(item, _document, finishedObjects); + FinishItemReferences(item, containingReference, _document, finishedObjects); } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureRenderer.cs new file mode 100644 index 00000000..5b0e01c5 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureRenderer.cs @@ -0,0 +1,48 @@ +using PdfSharp.Drawing; +using PdfSharp.Drawing.Layout; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PdfSharp.Pdf.Signatures +{ + internal class DefaultSignatureRenderer : ISignatureRenderer + { + public void Render(XGraphics gfx, XRect rect, PdfSignatureOptions options) + { + // if an image was provided, render only that + if (options.Image != null) + { + gfx.DrawImage(options.Image, 0, 0, rect.Width, rect.Height); + return; + } + + var sb = new StringBuilder(); + if (options.Signer != null) + { + sb.AppendFormat("Signed by {0}\n", options.Signer); + } + if (options.Location != null) + { + sb.AppendFormat("Location: {0}\n", options.Location); + } + if (options.Reason != null) + { + sb.AppendFormat("Reason: {0}\n", options.Reason); + } + sb.AppendFormat(CultureInfo.CurrentCulture, "Date: {0}", DateTime.Now); + + XFont font = new XFont("Verdana", 7, XFontStyleEx.Regular); + + XTextFormatter txtFormat = new XTextFormatter(gfx); + + txtFormat.DrawString(sb.ToString(), + font, + new XSolidBrush(XColor.FromKnownColor(XKnownColor.Black)), + new XRect(0, 0, rect.Width, 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..6e061dd5 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs @@ -0,0 +1,105 @@ +// 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.Net.Http.Headers; +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 readonly PdfSignatureOptions options; + + public DefaultSigner(PdfSignatureOptions signatureOptions) + { + if (signatureOptions?.Certificate is null) + throw new ArgumentException("Missing certificate in signature options"); + + options = signatureOptions; + } + + public byte[] GetSignedCms(Stream documentStream, PdfDocument document) + { + var range = new byte[documentStream.Length]; + documentStream.Position = 0; + documentStream.Read(range, 0, range.Length); + + return GetSignedCms(range, document); + } + + public byte[] GetSignedCms(byte[] range, PdfDocument document) + { + // Sign the byte range + var contentInfo = new ContentInfo(range); + var signedCms = new SignedCms(contentInfo, true); + var signer = new CmsSigner(options.Certificate)/* { IncludeOption = X509IncludeOption.WholeChain }*/; + signer.UnsignedAttributes.Add(new Pkcs9SigningTime()); + + signedCms.ComputeSignature(signer, true); + + if (options.TimestampAuthorityUri is not null) + Task.Run(() => AddTimestampFromTSAAsync(signedCms)).Wait(); + + var bytes = signedCms.Encode(); + + return bytes; + } + + public string? GetName() + { + return options.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 +#if NET6_0_OR_GREATER + // 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(options.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())); +#endif + } + } +} \ No newline at end of file diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISignatureRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISignatureRenderer.cs new file mode 100644 index 00000000..0dd7c40e --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISignatureRenderer.cs @@ -0,0 +1,9 @@ +using PdfSharp.Drawing; + +namespace PdfSharp.Pdf.Signatures +{ + public interface ISignatureRenderer + { + void Render(XGraphics gfx, XRect rect, PdfSignatureOptions options); + } +} 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..08bb848a --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs @@ -0,0 +1,11 @@ + +namespace PdfSharp.Pdf.Signatures +{ + public interface ISigner + { + byte[] GetSignedCms(Stream documentStream, PdfDocument document); + byte[] GetSignedCms(byte[] range, PdfDocument document); + + string? GetName(); + } +} 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..2e56a21c --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs @@ -0,0 +1,69 @@ +using PdfSharp.Drawing; +using System.Security.Cryptography.X509Certificates; + +namespace PdfSharp.Pdf.Signatures +{ + public class PdfSignatureOptions + { + /// + /// Certificate to sign the document with + /// + public X509Certificate2? Certificate { get; set; } + + /// + /// Uri of a timestamp authority used to get a timestamp from a trusted authority + /// + public Uri? TimestampAuthorityUri { get; set; } + + /// + /// The name of the signer.

+ /// If not set, defaults to the Subject of the provided Certificate + ///
+ public string? Signer { get; set; } + + /// + /// Contact info for the signer + /// + public string? ContactInfo { get; set; } + + /// + /// The location where the signing took place + /// + public string? Location { get; set; } + + /// + /// The reason for signing + /// + public string? Reason { get; set; } + + public bool Certify { get; set; } + + /// + /// Rectangle of the Signature-Field's Annotation.

+ /// Specify an empty rectangle to create an invisible signature. + ///
+ public XRect Rectangle { get; set; } = XRect.Empty; + + /// + /// Page index, zero-based. Only needed for visible signatures. + /// + public int PageIndex { get; set; } = 0; + + /// + /// The name of the Signature-Field.

+ /// If a field with that name already exist in the document, it will be used, otherwise it will be created.

+ /// Currently, only root-fields are supported (that is, the existing field is not allowed to be a child of another field) + ///
+ public string FieldName { get; set; } = "Signature1"; + + /// + /// An image to render as the Field's Annotation + /// + public XImage? Image { get; set; } + + /// + /// A custom appearance renderer for the signature + /// + public ISignatureRenderer? Renderer { get; set; } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureValue.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureValue.cs new file mode 100644 index 00000000..721f4e47 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureValue.cs @@ -0,0 +1,399 @@ +using PdfSharp.Internal; +using PdfSharp.Pdf.AcroForms; +using PdfSharp.Pdf.Internal; +using PdfSharp.Pdf.IO; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace PdfSharp.Pdf.Signatures +{ + /// + /// Defines the value for a + /// + public class PdfSignatureValue : PdfDictionary + { + /// + /// Used to report the positions of the values of and + /// when writing this field to a stream + /// + /// A reference to the value itself + /// The start-position of the value + /// The end-position of the value + internal delegate void SignatureWriteCallback(PdfSignatureValue signatureValue, SizeType start, SizeType end); + + internal SignatureWriteCallback? SignatureContentsWritten; + + internal SignatureWriteCallback? SignatureRangeWritten; + + internal PdfSignatureValue(PdfDocument document) + : base(document) + { + Elements.SetName(Keys.Type, "/Sig"); + } + + internal PdfSignatureValue(PdfDictionary dict) + : base(dict) + { } + + /// + /// (Required; inheritable) The name of the signature handler to be used for + /// authenticating the field’s contents, such as Adobe.PPKLite, Entrust.PPKEF, + /// CICI.SignIt, or VeriSign.PPKVS. + /// + public string Filter + { + get + { + var val = Elements.GetName(Keys.Filter); + return val; + } + set + { + Elements.SetName(Keys.Filter, value); + } + } + + /// + /// (Optional) A name that describes the encoding of the signature value and key + /// information in the signature dictionary.

+ /// A PDF processor may use any handler that supports this format to validate the signature. + ///
+ public string SubFilter + { + get + { + var val = Elements.GetName(Keys.SubFilter); + return val; + } + set + { + Elements.SetName(Keys.SubFilter, value); + } + } + + /// + /// (Optional) The name of the person or authority signing the document. + /// + public string Name + { + get + { + var val = Elements.GetString(Keys.Name); + return val; + } + set + { + Elements.SetString(Keys.Name, value); + } + } + + /// + /// (Optional) The CPU host name or physical location of the signing. + /// + public string Location + { + get + { + var val = Elements.GetString(Keys.Location); + return val; + } + set + { + Elements.SetString(Keys.Location, value); + } + } + + /// + /// (Optional) The reason for the signing, such as (I agree…). + /// + public string Reason + { + get + { + var val = Elements.GetString(Keys.Reason); + return val; + } + set + { + Elements.SetString(Keys.Reason, value); + } + } + + /// + /// (Optional) Information provided by the signer to enable a recipient to contact the signer to verify the signature.

+ /// If SubFilter is ETSI.RFC3161, this entry should not be used and should be ignored by a PDF processor. + ///
+ public string ContactInfo + { + get + { + var val = Elements.GetString(Keys.ContactInfo); + return val; + } + set + { + Elements.SetString(Keys.ContactInfo, value); + } + } + + /// + /// (Optional) The time of signing.

+ /// Depending on the signature handler, this may be a normal unverified computer time + /// or a time generated in a verifiable way from a secure time server. + ///
+ public DateTime SigningDate + { + get + { + var dt = Elements.GetDateTime(Keys.M, DateTime.UtcNow); + return dt; + } + set + { + Elements.SetDateTime(Keys.M, value); + } + } + + /// + /// (Required) An array of pairs of integers (starting byte offset, length in bytes) + /// describing the exact byte range for the digest calculation.

+ /// Multiple discontinuous byte ranges may be used to describe a digest that does not include the + /// signature token itself. + ///
+ public PdfArray? ByteRange + { + get + { + return Elements.GetArray(Keys.ByteRange); + } + set + { + if (value is not null) + Elements.SetObject(Keys.ByteRange, value); + else + Elements.Remove(Keys.ByteRange); + } + } + + /// + /// (Required) The encrypted signature token. + /// + public byte[] Contents + { + get + { + var str = Elements.GetString(Keys.Contents); + return PdfEncoders.RawEncoding.GetBytes(str); + } + set + { + var str = PdfEncoders.RawEncoding.GetString(value, 0, value.Length); + var hexStr = new PdfString(str, PdfStringFlags.HexLiteral); + Elements[Keys.Contents] = hexStr; + } + } + + /// + /// Writes a key/value pair of this signature field dictionary. + /// + internal override void WriteDictionaryElement(PdfWriter writer, PdfName key) + { + // Don’t encrypt Contents key’s value (PDF Reference 2.0: 7.6.2, Page 71). + if (key.Value == Keys.Contents) + { + var item = Elements[key]; + key.WriteObject(writer); + var start = writer.Position; + item?.WriteObject(writer); + var end = writer.Position; + writer.NewLine(); + SignatureContentsWritten?.Invoke(this, start, end); + + //var effectiveSecurityHandler = writer.EffectiveSecurityHandler; + //writer.EffectiveSecurityHandler = null; + //base.WriteDictionaryElement(writer, key); + //writer.EffectiveSecurityHandler = effectiveSecurityHandler; + } + else if (key.Value == Keys.ByteRange) + { + var item = Elements[key]; + key.WriteObject(writer); + var start = writer.Position; + item?.WriteObject(writer); + var end = writer.Position; + writer.NewLine(); + SignatureRangeWritten?.Invoke(this, start, end); + } + else + base.WriteDictionaryElement(writer, key); + } + + /// + /// Predefined keys of this dictionary.

+ /// PDF Reference 2.0, Chapter 12.8.1, Table 255

+ /// Consult the spec for more detailed information. + ///
+ public class Keys : KeysBase + { + /// + /// (Optional if Sig; Required if DocTimeStamp)

+ /// The type of PDF object that this dictionary describes; if present, shall be Sig for a signature dictionary or + /// DocTimeStamp for a timestamp signature dictionary.

+ /// The default value is: Sig. + ///
+ [KeyInfo(KeyType.Name | KeyType.Optional)] + public const string Type = "/Type"; + + /// + /// (Required; inheritable) The name of the signature handler to be used for + /// authenticating the field’s contents, such as Adobe.PPKLite, Entrust.PPKEF, + /// CICI.SignIt, or VeriSign.PPKVS. + /// + [KeyInfo(KeyType.Name | KeyType.Required)] + public const string Filter = "/Filter"; + + /// + /// (Optional) A name that describes the encoding of the signature value and key + /// information in the signature dictionary.

+ /// A PDF processor may use any handler that supports this format to validate the signature. + ///
+ [KeyInfo(KeyType.Name | KeyType.Optional)] + public const string SubFilter = "/SubFilter"; + + /// + /// (Required) An array of pairs of integers (starting byte offset, length in bytes) + /// describing the exact byte range for the digest calculation.

+ /// Multiple discontinuous byte ranges may be used to describe a digest that does not include the + /// signature token itself. + ///
+ [KeyInfo(KeyType.Array | KeyType.Required)] + public const string ByteRange = "/ByteRange"; + + /// + /// (Required) The encrypted signature token. + /// + [KeyInfo(KeyType.String | KeyType.Required)] + public const string Contents = "/Contents"; + + // Cert (deprecated ?) + + /// + /// (Optional; PDF 1.5) An array of signature reference dictionaries + /// (see "Table 256 — Entries in a signature reference dictionary").

+ /// If SubFilter is ETSI.RFC3161, this entry shall not be used. + ///
+ [KeyInfo(KeyType.Array | KeyType.Optional)] + public const string Reference = "/Reference"; + + /// + /// (Optional) An array of three integers that shall specify changes to the + /// document that have been made between the previous signature and this + /// signature: in this order, the number of pages altered, the number of fields altered, + /// and the number of fields filled in.

+ /// The ordering of signatures shall be determined by the value of ByteRange.

+ /// Since each signature results in an incremental save, later signatures have a + /// greater length value.

+ /// If SubFilter is ETSI.RFC3161, this entry shall not be used. + ///
+ [KeyInfo(KeyType.Array | KeyType.Optional)] + public const string Changes = "/Changes"; + + /// + /// (Optional) The name of the person or authority signing the document. + /// + [KeyInfo(KeyType.TextString | KeyType.Optional)] + public const string Name ="/Name"; + + /// + /// (Optional) The time of signing. Depending on the signature handler, this + /// may be a normal unverified computer time or a time generated in a verifiable + /// way from a secure time server. + /// + [KeyInfo(KeyType.Date | KeyType.Optional)] + public const string M = "/M"; + + /// + /// (Optional) The CPU host name or physical location of the signing. + /// + [KeyInfo(KeyType.TextString | KeyType.Optional)] + public const string Location = "/Location"; + + /// + /// (Optional) The reason for the signing, such as (I agree…). + /// + [KeyInfo(KeyType.TextString | KeyType.Optional)] + public const string Reason = "/Reason"; + + /// + /// (Optional) Information provided by the signer to enable a recipient to contact the signer to verify the signature.

+ /// If SubFilter is ETSI.RFC3161, this entry should not be used and should be ignored by a PDF processor. + ///
+ [KeyInfo(KeyType.TextString | KeyType.Optional)] + public const string ContactInfo = "/ContactInfo"; + + /// + /// (Optional; deprecated in PDF 2.0) The version of the signature handler that + /// was used to create the signature.

+ /// (PDF 1.5) This entry shall not be used, and the information shall be stored in the Prop_Build dictionary. + ///
+ [KeyInfo(KeyType.Integer | KeyType.Optional)] + public const string R = "/R"; + + /// + /// (Optional; PDF 1.5) The version of the signature dictionary format.

+ /// It corresponds to the usage of the signature dictionary in the context of the value of SubFilter.

+ /// The value is 1 if the Reference dictionary shall be considered critical to the validation of the signature.

+ /// If SubFilter is ETSI.RFC3161, this V value shall be 0 (possibly by default).

+ /// Default value: 0. + ///
+ [KeyInfo(KeyType.Integer | KeyType.Optional)] + public const string V = "/V"; + + /// + /// (Optional; PDF 1.5) A dictionary that may be used by a signature handler to + /// record information that captures the state of the computer environment used + /// for signing, such as the name of the handler used to create the signature, + /// software build date, version, and operating system.

+ /// The use of this dictionary is defined by Adobe PDF Signature Build Dictionary + /// Specification, which provides implementation guidelines. + ///
+ [KeyInfo(KeyType.Dictionary | KeyType.Optional)] + public const string Prop_Build = "/Prop_Build"; + + /// + /// (Optional; PDF 1.5) The number of seconds since the signer was last + /// authenticated, used in claims of signature repudiation.

+ /// It should be omitted if the value is unknown.

+ /// If SubFilter is ETSI.RFC3161, this entry shall not be used. + ///
+ [KeyInfo(KeyType.Integer | KeyType.Optional)] + public const string Prop_AuthTime = "/Prop_AuthTime"; + + /// + /// (Optional; PDF 1.5) The method that shall be used to authenticate the signer, + /// used in claims of signature repudiation.

+ /// Valid values shall be PIN, Password, and Fingerprint.

+ ///If SubFilter is ETSI.RFC3161, this entry shall not be used. + ///
+ [KeyInfo(KeyType.Name | KeyType.Optional)] + public const string Prop_AuthType = "/Prop_AuthType"; + + /// + /// Gets the KeysMeta for these keys. + /// + internal static DictionaryMeta Meta => _meta ??= CreateMeta(typeof(Keys)); + + static DictionaryMeta? _meta; + } + + /// + /// Gets the KeysMeta of this dictionary type. + /// + internal override DictionaryMeta Meta => Keys.Meta; + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSigner.cs new file mode 100644 index 00000000..901eeec0 --- /dev/null +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSigner.cs @@ -0,0 +1,256 @@ +using PdfSharp.Drawing; +using PdfSharp.Pdf.AcroForms; +using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.Internal; +using PdfSharp.Pdf.IO; +using System.Security.Cryptography.X509Certificates; + +namespace PdfSharp.Pdf.Signatures +{ + /// + /// Utility class for signing PDF-documents + /// + public class PdfSigner + { + private readonly Stream inputStream; + + private readonly PdfDocument document; + + private readonly ISigner signer; + + private readonly PdfSignatureOptions options; + + /// + /// Create new new instance for the specified document and with the specified options + /// + /// Stream specifying the document to sign. Must be readable and seekable + /// The options that spefify, how the signing is performed + /// + /// + public PdfSigner(Stream documentStream, PdfSignatureOptions signatureOptions) + { + if (documentStream is null) + throw new ArgumentNullException(nameof(documentStream)); + if (!documentStream.CanRead || !documentStream.CanSeek) + throw new ArgumentException("Invalid stream. Must be readable and seekable", nameof(documentStream)); + options = signatureOptions ?? throw new ArgumentNullException(nameof(signatureOptions)); + + if (options.Certificate is null) + throw new ArgumentException("A certificate is required to sign"); + if (options.PageIndex < 0) + throw new ArgumentException("Page index cannot be less than zero"); + + inputStream = documentStream; + document = PdfReader.Open(documentStream, PdfDocumentOpenMode.Append); + signer = new DefaultSigner(signatureOptions); + } + + /// + /// Signs the document + /// + /// A stream containing the signed document. Stream-position is 0 + public Stream Sign() + { + var signatureValue = CreateSignatureValue(); + var signatureField = GetOrCreateSignatureField(signatureValue); + RenderSignatureAppearance(signatureField); + + var finalDocumentLength = 0L; + var contentStart = 0L; + var contentEnd = 0L; + var rangeStart = 0L; + var rangeEnd = 0L; + var extraSpace = 0; + signatureValue.SignatureContentsWritten = (sigValue, start, end) => + { + contentStart = start; + contentEnd = end; + }; + signatureValue.SignatureRangeWritten = (sigValue, start, end) => + { + rangeStart = start; + rangeEnd = end; + }; + document.AfterSave = (writer) => + { + extraSpace = writer.Layout == PdfWriterLayout.Verbose ? 1 : 0; + }; + var ms = new MemoryStream(); + // copy original document to output-stream + inputStream.Seek(0, SeekOrigin.Begin); + inputStream.CopyTo(ms); + // append incremental update + document.Save(ms); + + finalDocumentLength = ms.Length; + + contentStart += extraSpace; + rangeStart += extraSpace; + + // write new ByteRange array + var rangeArrayValue = string.Format(CultureInfo.InvariantCulture, "[0 {0} {1} {2}]", + contentStart, contentEnd, finalDocumentLength - contentEnd); + Debug.Assert(rangeArrayValue.Length <= rangeEnd - rangeStart); + rangeArrayValue = rangeArrayValue.PadRight((int)(rangeEnd - rangeStart), ' '); + ms.Seek(rangeStart, SeekOrigin.Begin); + var writeBytes = PdfEncoders.RawEncoding.GetBytes(rangeArrayValue); + ms.Write(writeBytes, 0, writeBytes.Length); + + // concat the ranges before and after the content-string + var lengthToSign = contentStart + finalDocumentLength - contentEnd; + var toSign = new byte[lengthToSign]; + ms.Seek(0, SeekOrigin.Begin); + ms.Read(toSign, 0, (int)contentStart); + ms.Seek(contentEnd, SeekOrigin.Begin); + ms.Read(toSign, (int)contentStart, (int)(finalDocumentLength - contentEnd)); + + // do the signing + var signatureData = signer.GetSignedCms(toSign, document); + + // move past the '<' + ms.Seek(contentStart + 1, SeekOrigin.Begin); + // convert signature to string + var signHexString = PdfEncoders.ToHexStringLiteral(signatureData, false, false, null); + writeBytes = new byte[signHexString.Length - 2]; + // exclude '<' and '>' from hex-string and overwrite fake value + PdfEncoders.RawEncoding.GetBytes(signHexString, 1, signHexString.Length - 2, writeBytes, 0); + ms.Write(writeBytes, 0, writeBytes.Length); + + ms.Position = 0; + + document.Dispose(); + + return ms; + } + + private int GetContentLength() + { + return signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), document).Length + 10; + } + + private PdfSignatureField GetOrCreateSignatureField(PdfSignatureValue value) + { + var acroForm = document.GetOrCreateAcroForm(); + var fieldList = GetExistingFields(); + // if a field with the specified name exist, use that + // Note: only root-level fields are currently supported + var fieldWithName = fieldList.FirstOrDefault(f => f.Name == options.FieldName); + if (fieldWithName != null && !(fieldWithName is PdfSignatureField)) + throw new ArgumentException( + $"Field '{options.FieldName}' exist in document, but it is not a Signature-Field ({fieldWithName.GetType().Name})"); + + var isNewField = false; + var signatureField = fieldList.FirstOrDefault(f => + f is PdfSignatureField && f.Name == options.FieldName) as PdfSignatureField; + if (signatureField == null) + { + // field does not exist, create new one + signatureField = new PdfSignatureField(document) + { + Name = options.FieldName + }; + document.IrefTable.Add(signatureField); + acroForm.Fields.Elements.Add(signatureField); + isNewField = true; + } + // Flags: SignaturesExit + AppendOnly + acroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + + signatureField.Value = value; + signatureField.Elements.SetInteger(PdfAcroField.Keys.Ff, (int)PdfAcroFieldFlags.NoExport); + signatureField.Elements.SetName(PdfAnnotation.Keys.Type, "/Annot"); + signatureField.Elements.SetName(PdfAnnotation.Keys.Subtype, "/Widget"); + if (isNewField) + { + signatureField.Elements.SetReference("/P", document.Pages[options.PageIndex]); + signatureField.Elements.Add(PdfAnnotation.Keys.Rect, new PdfRectangle(options.Rectangle)); + } + 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 if (!annotations.Elements.Contains(signatureField)) + annotations.Elements.Add(signatureField); + + return signatureField; + } + + private PdfSignatureValue CreateSignatureValue() + { + var signatureDict = new PdfSignatureValue(document); + document.IrefTable.Add(signatureDict); + + var contentLength = GetContentLength(); + var content = Enumerable.Repeat(0, contentLength).ToArray(); + signatureDict.Contents = content; + signatureDict.Filter = "/Adobe.PPKLite"; + signatureDict.SubFilter = "/adbe.pkcs7.detached"; + signatureDict.SigningDate = DateTime.Now; + + var documentLength = inputStream.Length; + // fill with large enough fake values. we will overwrite these later + var byteRange = new PdfArray(document, new PdfLongInteger(0), new PdfLongInteger(documentLength), + new PdfLongInteger(documentLength), new PdfLongInteger(documentLength)); + signatureDict.ByteRange = byteRange; + if (options.Reason is not null) + signatureDict.Reason = options.Reason; + if (options.Location is not null) + signatureDict.Location = options.Location; + if (options.ContactInfo is not null) + signatureDict.ContactInfo = options.ContactInfo; + + return signatureDict; + } + + private void RenderSignatureAppearance(PdfSignatureField signatureField) + { + if (string.IsNullOrEmpty(options.Signer)) + options.Signer = signer.GetName() ?? "unknown"; + + XRect annotRect; + var rect = signatureField.Elements.GetRectangle(PdfAnnotation.Keys.Rect); + if (rect.IsEmpty) + { + // XRect.IsEmpty returns false even when width and height are zero ?? + if (options.Rectangle.Width <= 0 || options.Rectangle.Height <= 0) + return; + + annotRect = options.Rectangle; + signatureField.Elements.SetRectangle(PdfAnnotation.Keys.Rect, new PdfRectangle(annotRect)); + } + else + annotRect = rect.ToXRect(); + + var form = new XForm(document, annotRect.Size); + var gfx = XGraphics.FromForm(form); + var renderer = options.Renderer ?? new DefaultSignatureRenderer(); + renderer.Render(gfx, annotRect, options); + form.DrawingFinished(); + form.PdfRenderer.Close(); + + if (signatureField.Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap) + { + ap = new PdfDictionary(document); + signatureField.Elements.Add(PdfAnnotation.Keys.AP, ap); + } + ap.Elements.SetReference("/N", form.PdfForm); + } + + /// + /// Gets the list of existing root-fields of this document + /// + /// + private IEnumerable GetExistingFields() + { + var fields = new List(); + if (document.AcroForm?.Fields != null) + { + for (var i = 0; i < document.AcroForm.Fields.Count; i++) + { + var field = document.AcroForm.Fields[i]; + fields.Add(field); + } + } + return fields; + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index b1e52cb6..e6821937 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -25,6 +25,14 @@ namespace PdfSharp.Pdf [DebuggerDisplay("(Name={" + nameof(Name) + "})")] // A name makes debugging easier public sealed class PdfDocument : PdfObject, IDisposable { + /// + /// Used to report that saving the document has been finished. + /// + /// + internal delegate void AfterSaveCallback(PdfWriter writer); + + internal AfterSaveCallback AfterSave; + #if DEBUG_ static PdfDocument() { @@ -312,7 +320,7 @@ void DoSave(PdfWriter writer) { PdfSharpLogHost.Logger.PdfDocumentSaved(Name); - if (_pages == null || _pages.Count == 0) + if (Pages == null || Pages.Count == 0) { if (OutStream != null) { @@ -390,6 +398,8 @@ void DoSave(PdfWriter writer) if (writer != null!) { writer.Stream.Flush(); + + AfterSave?.Invoke(writer); // DO NOT CLOSE WRITER HERE } _state |= DocumentState.Saved; @@ -460,6 +470,15 @@ internal void SaveIncrementally(PdfWriter writer) if (key == PdfTrailer.Keys.Prev || key == PdfTrailer.Keys.Size) continue; newTrailer.Elements[key] = Trailer.Elements[key]; + if (key == PdfTrailer.Keys.ID) + { + // first id stays the same, second is updated for each update + var id1 = Trailer.GetDocumentID(0); + var docID = Guid.NewGuid().ToByteArray(); + string id2 = PdfEncoders.RawEncoding.GetString(docID, 0, docID.Length); + newTrailer.Elements.SetObject(PdfTrailer.Keys.ID, new PdfArray(this, + new PdfString(id1, PdfStringFlags.HexLiteral), new PdfString(id2, PdfStringFlags.HexLiteral))); + } } newTrailer.Size = IrefTable.MaxObjectNumber + 1; newTrailer.Elements.SetObject(PdfTrailer.Keys.Prev, new PdfLongIntegerObject(this, Trailer.Position)); @@ -737,7 +756,26 @@ public PdfPageMode PageMode /// /// Get the AcroForm dictionary. /// - public PdfAcroForm AcroForm => Catalog.AcroForm; + public PdfAcroForm? AcroForm => Catalog.AcroForm; + + /// + /// Gets the existing or creates a new one, if there is no in the current document + /// + /// The associated with this document + public PdfAcroForm GetOrCreateAcroForm() + { + var form = AcroForm; + if (form == null) + { + form = new PdfAcroForm(this); + IrefTable.Add(new PdfReference(form)); + if (form.Reference != null) + form.Reference.Document = this; + Catalog.AcroForm = form; + } + return form; + } + /// /// Gets or sets the default language of the document. @@ -914,7 +952,7 @@ public void AddEmbeddedFile(string name, Stream stream) /// public void Flatten() { - for (int idx = 0; idx < AcroForm.Fields.Count; idx++) + for (int idx = 0; idx < AcroForm?.Fields.Count; idx++) { AcroForm.Fields[idx].ReadOnly = true; } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj index cefdd52c..a2a3a0bd 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj +++ b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj @@ -58,4 +58,8 @@ + + + + diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/WriterTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/WriterTests.cs index 64354276..78678970 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/WriterTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/WriterTests.cs @@ -5,8 +5,10 @@ using PdfSharp.Drawing; using PdfSharp.Fonts; using PdfSharp.Pdf.IO; +using PdfSharp.Pdf.Signatures; using PdfSharp.Quality; using PdfSharp.Snippets.Font; +using System.Security.Cryptography.X509Certificates; using Xunit; namespace PdfSharp.Tests.IO @@ -60,5 +62,38 @@ public void Append_To_File() idx++; } } + + [Fact] + public void Sign() + { + var cert = new X509Certificate2(@"C:\Data\packdat.pfx", "1234"); + var options = new PdfSignatureOptions + { + Certificate = cert, + FieldName = "Signature-" + Guid.NewGuid().ToString("N"), + PageIndex = 0, + Rectangle = new XRect(120, 10, 100, 60), + Location = "My PC", + Reason = "Approving Rev #2", + // Signature appearances can also consist of an image (Rectangle should be adapted to image size) + //Image = XImage.FromFile(@"C:\Data\stamp.png") + }; + + // first signature + //var sourceFile = IOUtility.GetAssetsPath("archives/grammar-by-example/GBE/ReferencePDFs/WPF 1.31/Table-Layout.pdf")!; + //var targetFile = Path.Combine(Path.GetTempPath(), "AA-Signed.pdf"); + + // second signature + var sourceFile = Path.Combine(Path.GetTempPath(), "AA-Signed.pdf"); + var targetFile = Path.Combine(Path.GetTempPath(), "AA-Signed-2.pdf"); + File.Copy(sourceFile, targetFile, true); + + using var fs = File.Open(targetFile, FileMode.Open, FileAccess.ReadWrite); + var signer = new PdfSigner(fs, options); + var resultStream = signer.Sign(); + // overwrite input document + fs.Seek(0, SeekOrigin.Begin); + resultStream.CopyTo(fs); + } } }