Table of Contents

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 DXP6005 when 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.