Digital signatures
DocWright.Signatures verifies Office document signatures part by part, signs Office documents, and signs PDFs. DocWright never holds your private key. You implement a provider that signs a hash, so the key can live in a certificate store, a cloud KMS or an HSM.
dotnet add package DocWright.Signatures
Your signature provider
For Word documents, implement ISignatureProvider. It returns the certificate and signs bytes:
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using DocWright.Signatures;
/// <summary>Signs with a certificate you hold. DocWright never sees the private key.</summary>
internal sealed class CertificateSignatureProvider(X509Certificate2 certificate) : ISignatureProvider
{
public byte[] GetCertificate() => certificate.RawData;
public byte[] SignData(byte[] data, SignatureHashAlgorithm hashAlgorithm)
{
HashAlgorithmName name = hashAlgorithm switch
{
SignatureHashAlgorithm.Sha384 => HashAlgorithmName.SHA384,
SignatureHashAlgorithm.Sha512 => HashAlgorithmName.SHA512,
_ => HashAlgorithmName.SHA256,
};
using RSA key = certificate.GetRSAPrivateKey()!;
return key.SignData(data, name, RSASignaturePadding.Pkcs1);
}
}
Sign a document
using DocWright.Signatures;
using (FileStream source = File.OpenRead("quarterly-report.docx"))
using (FileStream destination = File.Create("report-signed.docx"))
{
PackageSigner.Sign(source, destination, new CertificateSignatureProvider(certificate), new PackageSigningOptions
{
HashAlgorithm = SignatureHashAlgorithm.Sha256,
SigningTime = new DateTimeOffset(2026, 10, 1, 9, 0, 0, TimeSpan.Zero), // omitted: no time recorded
});
}
There's no clock: without SigningTime, no time is recorded.
Verify
using DocWright.Signatures;
using (FileStream signed = File.OpenRead("report-signed.docx"))
{
PackageSignatureVerificationResult result = PackageSignatureVerifier.Verify(signed);
foreach (PackageSignature signature in result.Signatures)
{
Console.WriteLine($"{signature.PartUri}: {signature.Status}, signer {signature.SignerName}, "
+ $"{signature.References.Count} signed parts");
}
Console.WriteLine($"All signatures valid: {result.AllSignaturesValid}");
}
Every signed part is checked separately. So when a document has been changed, you learn which part changed, not just that something did:
using DocWright.Signatures;
using (FileStream tampered = File.OpenRead("report-tampered.docx"))
{
PackageSignatureVerificationResult result = PackageSignatureVerifier.Verify(tampered);
foreach (PackageSignature signature in result.Signatures)
{
Console.WriteLine($"{signature.Status}");
foreach (SignatureReferenceResult reference in signature.References.Where(r => !r.Matches))
{
Console.WriteLine($" changed: {reference.Target}: {reference.FailureReason}");
}
}
}
Output (signing, verifying, then verifying a copy with one word changed)
/_xmlsignatures/sig1.xml: Valid, signer CN=Acme Components Ltd, 6 signed parts
All signatures valid: True
ReferenceDigestMismatch
changed: /word/document.xml: The part does not match the digest recorded for it.
Signed PDF: 73,158 bytes
Important
"Valid" means intact, not trusted. Verification answers one question: do these bytes match what was signed with this certificate? It does not build a certificate chain, check revocation or decide whether to trust the signer. SignerCertificate gives you the certificate for your own trust checks. SigningTime is the signer's claim, not a proven time.
- Both signature locations, the OPC standard's and Office's
/_xmlsignatures/, are found. - SHA-1 signatures are verified, so you can find out that a document uses one, but never written.
- Saving a signed document breaks its signature, because the saved parts differ from what was signed. DocWright warns with
DXP6005when you load a signed document, and never re-signs on your behalf.
Sign a PDF
PDF signatures carry a detached CMS signature, so the provider is IPdfSignatureProvider. .NET's SignedCms, from the System.Security.Cryptography.Pkcs package, builds one:
dotnet add package System.Security.Cryptography.Pkcs
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using DocWright.Signatures;
/// <summary>PDF signatures carry a detached CMS signature.</summary>
internal sealed class CmsSignatureProvider(X509Certificate2 certificate) : IPdfSignatureProvider
{
public byte[] CreateDetachedCms(byte[] data, SignatureHashAlgorithm hashAlgorithm)
{
var cms = new SignedCms(new ContentInfo(data), detached: true);
cms.ComputeSignature(new CmsSigner(certificate) { IncludeOption = X509IncludeOption.EndCertOnly });
return cms.Encode();
}
}
using DocWright.Signatures;
byte[] pdf = File.ReadAllBytes("report.pdf");
using (FileStream signedPdf = File.Create("report-signed.pdf"))
{
PdfSigner.Sign(pdf, signedPdf, new CmsSignatureProvider(certificate), new PdfSigningOptions
{
Reason = "Approved for release",
Location = "Wellington",
});
}
The signature is added as an incremental update with an invisible signature field, so the signed PDF looks exactly like the unsigned one. PDFs that use cross-reference streams are refused with InvalidDocumentException. DocWright's own PDFs use classic tables and are always accepted.
Not supported: XAdES, countersignatures, signature policies and RFC 3161 timestamps.