How to Sign the SignatureLine using Office Open XML
Introduction
We can use PackageDigitalSignatureManager class available in System.IO.Packaging namespace to sign any part of an Open XML document. However, when we try to sign a document that has a signature line using this class, we will notice that though the document appears as signed, the signature line still appears as “needs to be signed”. In order to have the signature appear in the signature line, we will need to do some additional tasks which are mentioned in the resolution section below.
Steps to Duplicate the Behaviour
Create a new word document in Word 2007.
Save this document to C:\Test\Test.docx.
Insert a Signature Line by going to -> Insert tab-> in the Text group, point to the arrow next to Signature Line, and then click Microsoft Office Signature Line.
Use the following code to sign the document
// ========First function============ // Entry Point private void DigiSign() { // Open the Package using (Package package = Package.Open(<< Word File Location >>)) { // Get the certificate X509Certificate2 certificate = GetCertificate(); SignAllParts(package, certificate); } } // ========End Of First function============ // ========Second function============ private static void SignAllParts(Package package, X509Certificate certificate) { if (package == null) throw new ArgumentNullException("SignAllParts(package)"); List<Uri> PartstobeSigned = new List<Uri>(); List<PackageRelationshipSelector> SignableReleationships = new List<PackageRelationshipSelector>(); foreach (PackageRelationship relationship in package.GetRelationshipsByType(RT_OfficeDocument)) { // Pass the releationship of the root. This is decided based on the RT_OfficeDocument // https://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument CreateListOfSignableItems(relationship, PartstobeSigned, SignableReleationships); } // Create the DigitalSignature Manager PackageDigitalSignatureManager dsm = new PackageDigitalSignatureManager(package); dsm.CertificateOption = CertificateEmbeddingOption.InSignaturePart; try { dsm.Sign(PartstobeSigned, certificate, SignableReleationships); } catch (CryptographicException ex) { MessageBox.Show(ex.InnerException.ToString()); } }// end:SignAllParts() // ========End of Second function============ // ========Third function============ static void CreateListOfSignableItems(PackageRelationship relationship, List<Uri> PartstobeSigned, List<PackageRelationshipSelector> SignableReleationships) { // This function adds the releation to SignableReleationships. And then it gets the part based on the releationship. Parts URI gets added to the PartstobeSigned list. PackageRelationshipSelector selector = new PackageRelationshipSelector(relationship.SourceUri, PackageRelationshipSelectorType.Id, relationship.Id); SignableReleationships.Add(selector); if (relationship.TargetMode == TargetMode.Internal) { PackagePart part = relationship.Package.GetPart(PackUriHelper.ResolvePartUri(relationship.SourceUri, relationship.TargetUri)); if (PartstobeSigned.Contains(part.Uri) == false) { PartstobeSigned.Add(part.Uri); // GetRelationships Function: Returns a Collection Of all the releationships that are owned by the part. foreach (PackageRelationship childRelationship in part.GetRelationships()) { CreateListOfSignableItems(childRelationship, PartstobeSigned, SignableReleationships); } } } } // ========End of Third function============ // ======== Fourth function============ static X509Certificate2 GetCertificate() { X509Store certStore = new X509Store(StoreLocation.CurrentUser); certStore.Open(OpenFlags.ReadOnly); X509Certificate2Collection certs =X509Certificate2UI.SelectFromCollection(certStore.Certificates,"Select a certificate","Please select a certificate", X509SelectionFlag.SingleSelection); return certs.Count > 0 ? certs[0] : null; } // ========End of Fourth function============
Open the document, you will notice that the doc is signed but signature line still shows as “needs to be signed”.
Resolution
The point is, Sign method expects an XML file of ContentType “System.Security.Cryptography.Xml.DataObject” which should include the GUID of the signatureLine needs to be signed.
Note: Whenever we create a SignatureLine, it gets an unique GUID which we need to use while signing the SignatureLine.
The structure of the XML file should be
<SignatureProperties xmlns="https://www.w3.org/2000/09/xmldsig#">
<SignatureProperty Id="idOfficeV1Details" Target="#idPackageSignature">
<SignatureInfoV1 xmlns="https://schemas.microsoft.com/office/2006/digsig">
<SetupID>{3CF6B91E-C5F6-46A4-B036-72597274FCC0}</SetupID>
<SignatureText>Test User</SignatureText>
<ManifestHashAlgorithm>https://www.w3.org/2000/09/xmldsig#sha1</ManifestHashAlgorithm>
</SignatureInfoV1>
</SignatureProperty>
</SignatureProperties>
Where SetupID represents the GUID of the SignatureLine.
Once the XML file is created, we can call the Sign method available in PackageDigitalSignatureManager class. Following is the complete listing of the class:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO.Packaging;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Xml;
namespace DigitalSign
{
public partial class SignDoc
{
// Get help about them from https://download.microsoft.com/download/2/4/8/24862317-78F0-4C4B-B355-C7B2C1D997DB/%5BMS-OFFCRYPTO%5D.pdf
static readonly string RT_OfficeDocument = "https://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
static readonly string OfficeObjectID = "idOfficeObject";
static readonly string SignatureID = "idPackageSignature";
static readonly string ManifestHashAlgorithm = "https://www.w3.org/2000/09/xmldsig#sha1";
// Entry Point
private void DigiSign()
{
// Open the Package
using (Package package = Package.Open(<< Word File>>)
{
// Get the certificate
X509Certificate2 certificate = GetCertificate();
SignAllParts(package, certificate);
}
}
private static void SignAllParts(Package package, X509Certificate certificate)
{
if (package == null) throw new ArgumentNullException("SignAllParts(package)");
List<Uri> PartstobeSigned = new List<Uri>();
List<PackageRelationshipSelector> SignableReleationships = new List<PackageRelationshipSelector>();
foreach (PackageRelationship relationship in package.GetRelationshipsByType(RT_OfficeDocument))
{
// Pass the releationship of the root. This is decided based on the RT_OfficeDocument (https://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument)
CreateListOfSignableItems(relationship, PartstobeSigned, SignableReleationships);
}
// Create the DigitalSignature Manager
PackageDigitalSignatureManager dsm = new PackageDigitalSignatureManager(package);
dsm.CertificateOption = CertificateEmbeddingOption.InSignaturePart;
string signatureID = SignatureID;
string manifestHashAlgorithm = ManifestHashAlgorithm;
System.Security.Cryptography.Xml.DataObject officeObject = CreateOfficeObject(signatureID, manifestHashAlgorithm);
Reference officeObjectReference = new Reference("#" + OfficeObjectID);
try
{
dsm.Sign(PartstobeSigned, certificate, SignableReleationships, signatureID, new System.Security.Cryptography.Xml.DataObject[] { officeObject }, new Reference[] { officeObjectReference });
}
catch (CryptographicException ex)
{
MessageBox.Show(ex.InnerException.ToString());
}
}// end:SignAllParts()
/**************************SignDocument******************************/
// This function is a helper function. The main role of this function is to
// create two lists, one with Package Parts that you want to sign, the other
// containing PacakgeRelationshipSelector objects which indicate relationships to sign.
/*******************************************************************/
static void CreateListOfSignableItems(PackageRelationship relationship, List<Uri> PartstobeSigned, List<PackageRelationshipSelector> SignableReleationships)
{
// This function adds the releation to SignableReleationships. And then it gets the part based on the releationship. Parts URI gets added to the PartstobeSigned list.
PackageRelationshipSelector selector = new PackageRelationshipSelector(relationship.SourceUri, PackageRelationshipSelectorType.Id, relationship.Id);
SignableReleationships.Add(selector);
if (relationship.TargetMode == TargetMode.Internal)
{
PackagePart part = relationship.Package.GetPart(PackUriHelper.ResolvePartUri(relationship.SourceUri, relationship.TargetUri));
if (PartstobeSigned.Contains(part.Uri) == false)
{
PartstobeSigned.Add(part.Uri);
// GetRelationships Function: Returns a Collection Of all the releationships that are owned by the part.
foreach (PackageRelationship childRelationship in part.GetRelationships())
{
CreateListOfSignableItems(childRelationship, PartstobeSigned, SignableReleationships);
}
}
}
}
/**************************SignDocument******************************/
// Once you create the list and try to sign it, Office will not validate the Signature.
// To allow Office to validate the signature, it requires a custom object which should be added to the
// signature parts. This function loads the OfficeObject.xml resource.
// Please note that GUID being passed in document.Loadxml.
// Background Information: Once you add a SignatureLine in Word, Word gives a unique GUID to it. Now while loading the
// OfficeObject.xml, we need to make sure that The this GUID should match to the ID of the signature line.
// So if you are generating a SignatureLine programmtically, then mmake sure that you generate the GUID for the
// SignatureLine and for this element.
/*******************************************************************/
static System.Security.Cryptography.Xml.DataObject CreateOfficeObject(
string signatureID, string manifestHashAlgorithm)
{
XmlDocument document = new XmlDocument();
document.LoadXml(String.Format(Properties.Resources.OfficeObject, signatureID, manifestHashAlgorithm, "{3CF6B91E-C5F6-46A4-B036-72597274FCC0}"));
System.Security.Cryptography.Xml.DataObject officeObject = new System.Security.Cryptography.Xml.DataObject();
// do not change the order of the following two lines
officeObject.LoadXml(document.DocumentElement); // resets ID
officeObject.Id = OfficeObjectID; // required ID, do not change
return officeObject;
}
/********************************************************/
static X509Certificate2 GetCertificate()
{
X509Store certStore = new X509Store(StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certs =X509Certificate2UI.SelectFromCollection(certStore.Certificates,"Select a certificate","Please select a certificate",
X509SelectionFlag.SingleSelection);
return certs.Count > 0 ? certs[0] : null;
}
private void button2_Click(object sender, EventArgs e)
{
DialogResult a = openFileDialog1.ShowDialog();
openFileDialog1.InitialDirectory = "C:\\test";
textBox1.Text=openFileDialog1.FileName;
}
}
}
Please note that you need to change the GUID of the signatureLine used in the above sample. Following section shows you to how to get this information for an existing SignatureLine.
How to Check GUID of the SignatureLine
In order to know the GUID of the signatureLine which you have inserted using Word:
Extract the content of the package.
Go to SignatureLine declaration of document.xml. You will see a declaration like:
<o:signatureline v:ext="edit" id="{3CF6B91E-C5F6-46A4-B036-72597274FCC0}" provid="{00000000-0000-0000-0000-000000000000}" o:suggestedsigner="a" o:suggestedsigner2="a" o:suggestedsigneremail="a" issignatureline="t"/>
Where id attribute stands for the GUID. This GUID needs to be passed in the XML file while signing.
Reference for XML generation
Comments
Anonymous
May 20, 2014
Does not work on me. Are you sure this exact code works for you?Anonymous
May 27, 2014
Hi James, Yes, it did when the post was published. It was tested for Word 2007. Are you using Word 2007? We have not tested this for later versions of Word. Will do so and update the post for any changes. -PraveenAnonymous
July 22, 2014
The comment has been removed