Skip to content

Commit

Permalink
POC: Document signing based on empira#48 with support for multiple si…
Browse files Browse the repository at this point in the history
…gnatures
  • Loading branch information
packdat committed Jun 15, 2024
1 parent 63006a2 commit acd78bf
Show file tree
Hide file tree
Showing 16 changed files with 1,037 additions and 21 deletions.
4 changes: 4 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.0" />
<!-- Unit test packages -->
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="8.0.0" />
<PackageVersion Include="xunit.core" Version="2.8.1" />
<PackageVersion Include="xunit.assert" Version="2.4.2" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
Expand All @@ -99,6 +100,9 @@
<PackageVersion Include="coverlet.collector" Version="1.3.0" />
<PackageVersion Include="Moq" Version="4.16.1" />
<PackageVersion Include="NCrunch.Framework" Version="4.7.0.4" />
<!-- Signature packages -->
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="8.0.0" />
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.2.1" />
<!-- Other packages -->
<!--<PackageVersion Include="Dapper" Version="2.0.123" />-->
<PackageVersion Include="System.IO.Abstractions" Version="13.2.47" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public string Name
string name = Elements.GetString(Keys.T);
return name;
}
set
{
Elements.SetString(Keys.T, value);
}
}

/// <summary>
Expand Down Expand Up @@ -267,6 +271,10 @@ public PdfAcroFieldCollection Fields
/// </summary>
public sealed class PdfAcroFieldCollection : PdfArray
{
PdfAcroFieldCollection(PdfDocument document)
: base(document)
{ }

PdfAcroFieldCollection(PdfArray array)
: base(array)
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -15,32 +16,44 @@ public sealed class PdfSignatureField : PdfAcroField
/// </summary>
internal PdfSignatureField(PdfDocument document)
: base(document)
{ }
{
Elements[PdfAcroField.Keys.FT] = new PdfName("/Sig");
}

internal PdfSignatureField(PdfDictionary dict)
: base(dict)
{ }

/// <summary>
/// Writes a key/value pair of this signature field dictionary.
/// Gets or sets the value for this field
/// </summary>
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;

/// <summary>
/// Predefined keys of this dictionary.
/// The description comes from PDF 1.4 Reference.
/// The description comes from PDF 1.4 Reference.<br></br>
/// TODO: These are wrong !
/// The keys are for a <see cref="PdfSignatureValue"/>, not for a <see cref="PdfSignatureField"/>
/// </summary>
public new class Keys : PdfAcroField.Keys
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,31 @@ public PdfNameDictionary Names
/// <summary>
/// Gets the AcroForm dictionary of this document.
/// </summary>
public PdfAcroForm AcroForm
public PdfAcroForm? AcroForm
{
get
{
if (_acroForm == null)
_acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm)??NRT.ThrowOnNull<PdfAcroForm>();
_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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ??
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using PdfSharp.Drawing;

namespace PdfSharp.Pdf.Signatures
{
public interface ISignatureRenderer
{
void Render(XGraphics gfx, XRect rect, PdfSignatureOptions options);
}
}
11 changes: 11 additions & 0 deletions src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit acd78bf

Please sign in to comment.