Skip to content
Sign and Configure DocMDP Restriction

Sign and Configure DocMDP Restriction

DocMDP (document modification detection and prevention) turns an ordinary signature into a certifying one: besides signing, it declares which kinds of later change are acceptable. A validator that understands DocMDP compares each revision written after the signature against that declaration and reports the signature as invalid if anything outside it changed.

You get a DocMDP signature by wrapping an ordinary signature handler in sighandler.NewDocMDPHandler and giving it a permission level.

Permission levels

The four levels come from model/mdp and are numbered as in the PDF specification. What each one actually permits is decided by the diff policy; these are the rules of the default policy, which is what you get unless you supply your own.

LevelPPermitted, reported as warningsRejected, reported as errors
mdp.NoRestrictions0Fields added, changed or removed. Annotations added, changed or removed.Any page added, removed or replaced.
mdp.NoChanges1Nothing.Everything the policy tracks.
mdp.FillForms2Fields added or changed. Widget annotations added or changed.Fields removed. Any annotation removed. Non-widget annotations added or changed. Page changes.
mdp.FillFormsAndAnnots3Fields added or changed. Any annotation added, changed or removed.Fields removed. Page changes.

P is the value written into the signature’s transform parameters, and what GetDocMDPPermission gives back.

FillForms is the level to pick when the document is a form that recipients are meant to complete and sign. Field and widget changes are what filling in and signing look like at the object level, which is why they pass. FillFormsAndAnnots adds review markup: comments, highlights, stamps. NoChanges is for a finished document, and NoRestrictions records a level without enforcing much, though even it treats page-level changes as errors.

Note that FillForms permits adding and changing annotations only for widgets, but rejects removing any annotation, widgets included.

Signing with a DocMDP restriction

innerHandler, err := sighandler.NewAdobePKCS7Detached(priv, cert)
if err != nil {
    return err
}

handler, err := sighandler.NewDocMDPHandler(innerHandler, mdp.FillForms)
if err != nil {
    return err
}

signature := model.NewPdfSignature(handler)
signature.SetName("Test Signature Appearance Name")
signature.SetReason("TestSignatureAppearance Reason")
signature.SetDate(time.Now(), "")

if err := signature.Initialize(); err != nil {
    return err
}

The inner handler does all the cryptography; the DocMDP wrapper only appends a signature reference dictionary carrying the DocMDP transform method and the permission number. Any handler works underneath, PAdES included.

appender.Sign notices the DocMDP transform on the field and sets the catalog’s /Perms /DocMDP entry for you. That entry is how a validator later finds the level without having to know it in advance:

perms := reader.GetPerms()
if perms == nil || perms.DocMDP == nil {
    return errors.New("unexpected perms object")
}

docMDPPerm, ok := perms.DocMDP.GetDocMDPPermission()

Limitations

The permission level is fixed when the signature is created. There is no way to relax or tighten it afterwards without signing again.

DocMDPHandler.Validate always returns impossible validation without parse, because the check needs the document’s revisions and not just the signature. Validate through reader.ValidateSignatures, which detects the handler and calls ValidateWithOpts with the parser for you. Calling Validate directly gets you nothing but the error.

ValidateSignatures always uses the default diff policy. To supply your own implementation of mdp.DiffPolicy, call ValidateWithOpts yourself with a populated model.SignatureHandlerDocMDPParams.

The default policy only inspects pages, the page tree, annotations and form fields. Changes to page content streams are not compared, so redrawing what appears on a page is not reported at any permission level. DocMDP as implemented here detects structural change, not visual change.

Also, appender.Sign writes the /Perms entry for whichever signature carries the DocMDP transform, and does not check whether it is the first signature in the document.

Run the example

The example generates a throwaway RSA key and self-signed certificate in generateSigKeys, then addSignature signs the last page with mdp.FillForms and writes the file. It does not validate the result; the valid changes and invalid changes guides do that.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures
go run pdf_sign_docmdp.go <INPUT_PDF_PATH> <OUTPUT_PDF_PATH>

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source

Sample output

The visible appearance is an ordinary signature field; the restriction itself lives in the signature dictionary and the catalog.

Signed with DocMDP

Last updated on