Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,22 @@ if err := doc.SetField("Comments", ""); err != nil {
doc.Sign(signer, cert).Reason("Form Filled").Write(output)
```

#### Sign an Existing Signature Field
By default each signature gets a new field. To sign an unsigned signature field
that is already in the document, such as one placed by a form editor, name it
by its fully qualified name. The field keeps its widgets and appearance, so
`Field` cannot be combined with `Appearance`.

```go
doc.Sign(signer, cert).Field("Signature1")
doc.Sign(signer2, cert2).Field("parties.buyer")
if _, err := doc.Write(output); err != nil {
log.Fatal(err)
}
```

With the `sign` package, set `SignData.FieldName`.

### Signature Extraction

You can iterate over all signatures in a document to inspect their properties or extract raw data without performing full cryptographic verification.
Expand Down
60 changes: 60 additions & 0 deletions document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,66 @@ func TestWrite_MultipleStagedSignatures(t *testing.T) {
}
}

// TestWrite_SignExistingFields signs the existing signature fields of a
// document one after another through the fluent API: each signature lands in
// the field it names and no new field is added.
func TestWrite_SignExistingFields(t *testing.T) {
pki := testpki.NewTestPKI(t)
pki.StartCRLServer()
defer pki.Close()

keyAlice, certAlice := pki.IssueLeaf("Alice")
keyBob, certBob := pki.IssueLeaf("Bob")

doc, err := OpenFile("testfiles/testfile_signature_fields.pdf")
if err != nil {
t.Fatalf("OpenFile: %v", err)
}

doc.Sign(keyAlice, certAlice, pki.Chain()...).Format(PAdES_B).Field("Signature1")
doc.Sign(keyBob, certBob, pki.Chain()...).Format(PAdES_B).Field("parent.child")

outPath := t.TempDir() + "/fields.pdf"
out, err := os.Create(outPath)
if err != nil {
t.Fatalf("failed to create output file: %v", err)
}
if _, err := doc.Write(out); err != nil {
t.Fatalf("Write: %v", err)
}
if err := out.Close(); err != nil {
t.Fatalf("close output: %v", err)
}

signedDoc, err := OpenFile(outPath)
if err != nil {
t.Fatalf("re-opening signed document: %v", err)
}

vr := signedDoc.Verify().TrustSelfSigned(true)
if err := vr.Err(); err != nil {
t.Fatalf("verify: %v", err)
}
if vr.Count() != 2 {
t.Fatalf("expected 2 signatures, got %d", vr.Count())
}
for _, sig := range vr.Signatures() {
if !sig.Valid {
t.Errorf("signature %s invalid: %v", sig.SignerName, sig.Errors)
}
}

var signatureFields []string
for _, field := range signedDoc.FormFields() {
if field.Type == "Sig" {
signatureFields = append(signatureFields, field.Name)
}
}
if len(signatureFields) != 2 || signatureFields[0] != "Signature1" || signatureFields[1] != "parent.child" {
t.Errorf("expected signature fields [Signature1 parent.child], got %v", signatureFields)
}
}

// TestAddInitials_PreservesInlinePageAttributes is a regression test for a
// bug in addAnnotToPage where inline (non-indirect) page attributes -
// Resources, Rotate, and each element of MediaBox/CropBox - were corrupted
Expand Down
2 changes: 2 additions & 0 deletions execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ func (d *Document) Write(output io.Writer) (*Result, error) {
signData.TSA.Password = sb.tsaPass
}

signData.FieldName = sb.fieldName

// Appearance configuration
if sb.appearance != nil {
signData.Appearance.Visible = true
Expand Down
11 changes: 7 additions & 4 deletions sign/pdfcatalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,14 @@ func (context *SignContext) createCatalog() ([]byte, error) {
}
}

// Add the visual signature field to the AcroForm dictionary
if fieldsAdded > 0 {
catalog_buffer.WriteString(" ")
// Add the visual signature field to the AcroForm dictionary. An existing
// field that is signed in place is already part of the field tree.
if context.SignData.FieldName == "" {
if fieldsAdded > 0 {
catalog_buffer.WriteString(" ")
}
catalog_buffer.WriteString(strconv.Itoa(int(context.VisualSignData.objectId)) + " 0 R")
}
catalog_buffer.WriteString(strconv.Itoa(int(context.VisualSignData.objectId)) + " 0 R")

catalog_buffer.WriteString("]\n") // close Fields array

Expand Down
85 changes: 85 additions & 0 deletions sign/pdfsignaturefield.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package sign

import (
"bytes"
"fmt"

"github.com/digitorus/pdf"
"github.com/digitorus/pdfsign/internal/acroform"
)

// fillSignatureField points the existing signature field named by
// SignData.FieldName at the new signature dictionary. The field is rewritten
// in the incremental update with all of its entries kept and /V added. Its
// widgets already sit in the page /Annots, so neither they nor the page are
// touched, and the field stays where it is in the AcroForm field tree.
func (context *SignContext) fillSignatureField() error {
field, err := context.findSignatureField(context.SignData.FieldName)
if err != nil {
return err
}

update, err := context.createFilledSignatureField(field)
if err != nil {
return err
}

fieldObjId := field.GetPtr().GetID()
if err := context.UpdateObject(fieldObjId, update); err != nil {
return fmt.Errorf("failed to update signature field %q: %w", context.SignData.FieldName, err)
}
context.VisualSignData.objectId = fieldObjId

return nil
}

// findSignatureField returns the dictionary of the terminal, unsigned
// signature field with the given fully qualified name.
func (context *SignContext) findSignatureField(name string) (pdf.Value, error) {
var field *acroform.Field
acroform.Fields(context.PDFReader.Trailer().Key("Root"), func(f acroform.Field) bool {
if f.Name != name {
return true
}
field = &f
return false
})

switch {
case field == nil:
return pdf.Value{}, fmt.Errorf("signature field %q not found", name)
case field.Type != "Sig":
return pdf.Value{}, fmt.Errorf("field %q is not a signature field (/FT /%s)", name, field.Type)
case !field.Terminal:
return pdf.Value{}, fmt.Errorf("signature field %q has child fields, name one of them instead", name)
case !field.Value.IsNull():
return pdf.Value{}, fmt.Errorf("signature field %q is already signed", name)
case field.Dict.GetPtr().GetID() == 0:
return pdf.Value{}, fmt.Errorf("signature field %q is a direct object and cannot be updated incrementally", name)
}

return field.Dict, nil
}

// createFilledSignatureField serializes the field dictionary with a /V entry
// that references the new signature dictionary.
func (context *SignContext) createFilledSignatureField(field pdf.Value) ([]byte, error) {
var buf bytes.Buffer
buf.WriteString("<<\n")

// Direct values carry the field's own pointer, which tells them apart
// from references to other objects such as /P, /Parent and /AP.
fieldObjId := field.GetPtr().GetID()
for _, key := range field.Keys() {
_, _ = fmt.Fprintf(&buf, " %s ", pdfName(key))
if err := context.serializeCatalogEntry(&buf, fieldObjId, field.Key(key)); err != nil {
return nil, fmt.Errorf("failed to serialize signature field entry %q: %w", key, err)
}
buf.WriteString("\n")
}

_, _ = fmt.Fprintf(&buf, " /V %d 0 R\n", context.SignData.objectId)
buf.WriteString(">>\n")

return buf.Bytes(), nil
}
Loading