Skip to content

Commit acd78bf

Browse files
committed
POC: Document signing based on empira#48 with support for multiple signatures
1 parent 63006a2 commit acd78bf

File tree

16 files changed

+1037
-21
lines changed

16 files changed

+1037
-21
lines changed

src/Directory.Packages.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.0" />
9090
<!-- Unit test packages -->
9191
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
92+
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="8.0.0" />
9293
<PackageVersion Include="xunit.core" Version="2.8.1" />
9394
<PackageVersion Include="xunit.assert" Version="2.4.2" />
9495
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
@@ -99,6 +100,9 @@
99100
<PackageVersion Include="coverlet.collector" Version="1.3.0" />
100101
<PackageVersion Include="Moq" Version="4.16.1" />
101102
<PackageVersion Include="NCrunch.Framework" Version="4.7.0.4" />
103+
<!-- Signature packages -->
104+
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="8.0.0" />
105+
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.2.1" />
102106
<!-- Other packages -->
103107
<!--<PackageVersion Include="Dapper" Version="2.0.123" />-->
104108
<PackageVersion Include="System.IO.Abstractions" Version="13.2.47" />

src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public string Name
3434
string name = Elements.GetString(Keys.T);
3535
return name;
3636
}
37+
set
38+
{
39+
Elements.SetString(Keys.T, value);
40+
}
3741
}
3842

3943
/// <summary>
@@ -267,6 +271,10 @@ public PdfAcroFieldCollection Fields
267271
/// </summary>
268272
public sealed class PdfAcroFieldCollection : PdfArray
269273
{
274+
PdfAcroFieldCollection(PdfDocument document)
275+
: base(document)
276+
{ }
277+
270278
PdfAcroFieldCollection(PdfArray array)
271279
: base(array)
272280
{ }

src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See the LICENSE file in the solution root for more information.
33

44
using PdfSharp.Pdf.IO;
5+
using PdfSharp.Pdf.Signatures;
56

67
namespace PdfSharp.Pdf.AcroForms
78
{
@@ -15,32 +16,44 @@ public sealed class PdfSignatureField : PdfAcroField
1516
/// </summary>
1617
internal PdfSignatureField(PdfDocument document)
1718
: base(document)
18-
{ }
19+
{
20+
Elements[PdfAcroField.Keys.FT] = new PdfName("/Sig");
21+
}
1922

2023
internal PdfSignatureField(PdfDictionary dict)
2124
: base(dict)
2225
{ }
2326

2427
/// <summary>
25-
/// Writes a key/value pair of this signature field dictionary.
28+
/// Gets or sets the value for this field
2629
/// </summary>
27-
internal override void WriteDictionaryElement(PdfWriter writer, PdfName key)
30+
public new PdfSignatureValue? Value
2831
{
29-
// Don’t encrypt Contents key’s value (PDF Reference 2.0: 7.6.2, Page 71).
30-
if (key.Value == Keys.Contents)
32+
get
33+
{
34+
if (sigValue is null)
35+
{
36+
var dict = Elements.GetValue(PdfAcroField.Keys.V) as PdfDictionary;
37+
if (dict is not null)
38+
sigValue = new PdfSignatureValue(dict);
39+
}
40+
return sigValue;
41+
}
42+
set
3143
{
32-
var effectiveSecurityHandler = writer.EffectiveSecurityHandler;
33-
writer.EffectiveSecurityHandler = null;
34-
base.WriteDictionaryElement(writer, key);
35-
writer.EffectiveSecurityHandler = effectiveSecurityHandler;
44+
if (value is not null)
45+
Elements.SetReference(PdfAcroField.Keys.V, value);
46+
else
47+
Elements.Remove(PdfAcroField.Keys.V);
3648
}
37-
else
38-
base.WriteDictionaryElement(writer, key);
3949
}
50+
PdfSignatureValue? sigValue;
4051

4152
/// <summary>
4253
/// Predefined keys of this dictionary.
43-
/// The description comes from PDF 1.4 Reference.
54+
/// The description comes from PDF 1.4 Reference.<br></br>
55+
/// TODO: These are wrong !
56+
/// The keys are for a <see cref="PdfSignatureValue"/>, not for a <see cref="PdfSignatureField"/>
4457
/// </summary>
4558
public new class Keys : PdfAcroField.Keys
4659
{

src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,31 @@ public PdfNameDictionary Names
152152
/// <summary>
153153
/// Gets the AcroForm dictionary of this document.
154154
/// </summary>
155-
public PdfAcroForm AcroForm
155+
public PdfAcroForm? AcroForm
156156
{
157157
get
158158
{
159159
if (_acroForm == null)
160-
_acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm)??NRT.ThrowOnNull<PdfAcroForm>();
160+
_acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm);
161161
return _acroForm;
162162
}
163+
internal set
164+
{
165+
if (value != null)
166+
{
167+
if (!value.IsIndirect)
168+
throw new InvalidOperationException("Setting the AcroForm requires an indirect object");
169+
Elements.SetReference(Keys.AcroForm, value);
170+
_acroForm = value;
171+
}
172+
else
173+
{
174+
if (AcroForm != null && AcroForm.Reference != null)
175+
_document.IrefTable.Remove(AcroForm.Reference);
176+
Elements.Remove(Keys.AcroForm);
177+
_acroForm = null;
178+
}
179+
}
163180
}
164181
PdfAcroForm? _acroForm;
165182

src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public void Add(PdfObject value)
118118
ObjectTable.Add(value.ObjectID, value.ReferenceNotNull);
119119

120120
if (ReadyForModification && _document.IsAppending)
121-
ModifiedObjects[value.ObjectID] = value.Reference;
121+
ModifiedObjects[value.ObjectID] = value.ReferenceNotNull;
122122
}
123123

124124
/// <summary>

src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ void FinishReferences()
450450
"All references saved in IrefTable should have been created when their referred PdfObject has been accessible.");
451451

452452
// Get and update object’s references.
453-
FinishItemReferences(iref.Value, _document, finishedObjects);
453+
FinishItemReferences(iref.Value, iref, _document, finishedObjects);
454454
}
455455

456456
// why setting it here AND in Trailer.Finish ??
@@ -526,7 +526,7 @@ void FinishChildReferences(PdfDictionary dictionary, PdfReference containingRefe
526526
}
527527

528528
// Get and update item’s references.
529-
FinishItemReferences(item, _document, finishedObjects);
529+
FinishItemReferences(item, containingReference, _document, finishedObjects);
530530
}
531531
}
532532

@@ -549,7 +549,7 @@ void FinishChildReferences(PdfArray array, PdfReference containingReference, Has
549549
}
550550

551551
// Get and update item’s references.
552-
FinishItemReferences(item, _document, finishedObjects);
552+
FinishItemReferences(item, containingReference, _document, finishedObjects);
553553
}
554554
}
555555

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using PdfSharp.Drawing;
2+
using PdfSharp.Drawing.Layout;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace PdfSharp.Pdf.Signatures
10+
{
11+
internal class DefaultSignatureRenderer : ISignatureRenderer
12+
{
13+
public void Render(XGraphics gfx, XRect rect, PdfSignatureOptions options)
14+
{
15+
// if an image was provided, render only that
16+
if (options.Image != null)
17+
{
18+
gfx.DrawImage(options.Image, 0, 0, rect.Width, rect.Height);
19+
return;
20+
}
21+
22+
var sb = new StringBuilder();
23+
if (options.Signer != null)
24+
{
25+
sb.AppendFormat("Signed by {0}\n", options.Signer);
26+
}
27+
if (options.Location != null)
28+
{
29+
sb.AppendFormat("Location: {0}\n", options.Location);
30+
}
31+
if (options.Reason != null)
32+
{
33+
sb.AppendFormat("Reason: {0}\n", options.Reason);
34+
}
35+
sb.AppendFormat(CultureInfo.CurrentCulture, "Date: {0}", DateTime.Now);
36+
37+
XFont font = new XFont("Verdana", 7, XFontStyleEx.Regular);
38+
39+
XTextFormatter txtFormat = new XTextFormatter(gfx);
40+
41+
txtFormat.DrawString(sb.ToString(),
42+
font,
43+
new XSolidBrush(XColor.FromKnownColor(XKnownColor.Black)),
44+
new XRect(0, 0, rect.Width, rect.Height),
45+
XStringFormats.TopLeft);
46+
}
47+
}
48+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// PDFsharp - A .NET library for processing PDF
2+
// See the LICENSE file in the solution root for more information.
3+
4+
#if WPF
5+
using System.IO;
6+
#endif
7+
using System.Net.Http.Headers;
8+
using System.Security.Cryptography;
9+
using System.Security.Cryptography.Pkcs;
10+
using System.Security.Cryptography.X509Certificates;
11+
12+
namespace PdfSharp.Pdf.Signatures
13+
{
14+
public class DefaultSigner : ISigner
15+
{
16+
private static readonly Oid SignatureTimeStampOin = new Oid("1.2.840.113549.1.9.16.2.14");
17+
private static readonly string TimestampQueryContentType = "application/timestamp-query";
18+
private static readonly string TimestampReplyContentType = "application/timestamp-reply";
19+
20+
private readonly PdfSignatureOptions options;
21+
22+
public DefaultSigner(PdfSignatureOptions signatureOptions)
23+
{
24+
if (signatureOptions?.Certificate is null)
25+
throw new ArgumentException("Missing certificate in signature options");
26+
27+
options = signatureOptions;
28+
}
29+
30+
public byte[] GetSignedCms(Stream documentStream, PdfDocument document)
31+
{
32+
var range = new byte[documentStream.Length];
33+
documentStream.Position = 0;
34+
documentStream.Read(range, 0, range.Length);
35+
36+
return GetSignedCms(range, document);
37+
}
38+
39+
public byte[] GetSignedCms(byte[] range, PdfDocument document)
40+
{
41+
// Sign the byte range
42+
var contentInfo = new ContentInfo(range);
43+
var signedCms = new SignedCms(contentInfo, true);
44+
var signer = new CmsSigner(options.Certificate)/* { IncludeOption = X509IncludeOption.WholeChain }*/;
45+
signer.UnsignedAttributes.Add(new Pkcs9SigningTime());
46+
47+
signedCms.ComputeSignature(signer, true);
48+
49+
if (options.TimestampAuthorityUri is not null)
50+
Task.Run(() => AddTimestampFromTSAAsync(signedCms)).Wait();
51+
52+
var bytes = signedCms.Encode();
53+
54+
return bytes;
55+
}
56+
57+
public string? GetName()
58+
{
59+
return options.Certificate?.GetNameInfo(X509NameType.SimpleName, false);
60+
}
61+
62+
private async Task AddTimestampFromTSAAsync(SignedCms signedCms)
63+
{
64+
// Generate our nonce to identify the pair request-response
65+
byte[] nonce = new byte[8];
66+
#if NET6_0_OR_GREATER
67+
nonce = RandomNumberGenerator.GetBytes(8);
68+
#else
69+
using var cryptoProvider = new RNGCryptoServiceProvider();
70+
cryptoProvider.GetBytes(nonce = new Byte[8]);
71+
#endif
72+
#if NET6_0_OR_GREATER
73+
// Get our signing information and create the RFC3161 request
74+
SignerInfo newSignerInfo = signedCms.SignerInfos[0];
75+
// Now we generate our request for us to send to our RFC3161 signing authority.
76+
var request = Rfc3161TimestampRequest.CreateFromSignerInfo(
77+
newSignerInfo,
78+
HashAlgorithmName.SHA256,
79+
requestSignerCertificates: true, // ask TSA to embed its signing certificate in the timestamp token
80+
nonce: nonce);
81+
82+
var client = new HttpClient();
83+
var content = new ReadOnlyMemoryContent(request.Encode());
84+
content.Headers.ContentType = new MediaTypeHeaderValue(TimestampQueryContentType);
85+
var httpResponse = await client.PostAsync(options.TimestampAuthorityUri, content).ConfigureAwait(false);
86+
87+
// Process our response
88+
if (!httpResponse.IsSuccessStatusCode)
89+
{
90+
throw new CryptographicException(
91+
$"There was a error from the timestamp authority. It responded with {httpResponse.StatusCode} {(int)httpResponse.StatusCode}: {httpResponse.Content}");
92+
}
93+
if (httpResponse.Content.Headers.ContentType?.MediaType != TimestampReplyContentType)
94+
{
95+
throw new CryptographicException("The reply from the time stamp server was in a invalid format.");
96+
}
97+
var data = await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
98+
var timestampToken = request.ProcessResponse(data, out _);
99+
100+
// The RFC3161 sign certificate is separate to the contents that was signed, we need to add it to the unsigned attributes.
101+
newSignerInfo.AddUnsignedAttribute(new AsnEncodedData(SignatureTimeStampOin, timestampToken.AsSignedCms().Encode()));
102+
#endif
103+
}
104+
}
105+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using PdfSharp.Drawing;
2+
3+
namespace PdfSharp.Pdf.Signatures
4+
{
5+
public interface ISignatureRenderer
6+
{
7+
void Render(XGraphics gfx, XRect rect, PdfSignatureOptions options);
8+
}
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+

2+
namespace PdfSharp.Pdf.Signatures
3+
{
4+
public interface ISigner
5+
{
6+
byte[] GetSignedCms(Stream documentStream, PdfDocument document);
7+
byte[] GetSignedCms(byte[] range, PdfDocument document);
8+
9+
string? GetName();
10+
}
11+
}

0 commit comments

Comments
 (0)