Add initial support for signature appearance

This commit is contained in:
Paul van Brouwershaven
2024-12-16 15:05:27 +01:00
parent e5cdb61bea
commit b9112bb85b
10 changed files with 335 additions and 122 deletions

62
sign/appearance.go Normal file
View File

@@ -0,0 +1,62 @@
package sign
import (
"bytes"
"fmt"
)
func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) {
text := context.SignData.Signature.Info.Name
rectWidth := rect[2] - rect[0]
rectHeight := rect[3] - rect[1]
if rectWidth < 1 || rectHeight < 1 {
return nil, fmt.Errorf("invalid rectangle dimensions: width %.2f and height %.2f must be greater than 0", rectWidth, rectHeight)
}
// Calculate font size
fontSize := rectHeight * 0.8 // Initial font size
textWidth := float64(len(text)) * fontSize * 0.5 // Approximate text width
if textWidth > rectWidth {
fontSize = rectWidth / (float64(len(text)) * 0.5) // Adjust font size to fit text within rect width
}
var appearance_stream_buffer bytes.Buffer
appearance_stream_buffer.WriteString("q\n") // Save graphics state
appearance_stream_buffer.WriteString("BT\n") // Begin text
appearance_stream_buffer.WriteString(fmt.Sprintf("/F1 %.2f Tf\n", fontSize)) // Font and size
appearance_stream_buffer.WriteString(fmt.Sprintf("0 %.2f Td\n", rectHeight-fontSize)) // Position in unit square
appearance_stream_buffer.WriteString("0.2 0.2 0.6 rg\n") // Set font color to ballpoint-like color (RGB)
appearance_stream_buffer.WriteString(fmt.Sprintf("%s Tj\n", pdfString(text))) // Show text
appearance_stream_buffer.WriteString("ET\n") // End text
appearance_stream_buffer.WriteString("Q\n") // Restore graphics state
var appearance_buffer bytes.Buffer
appearance_buffer.WriteString("<<\n")
appearance_buffer.WriteString(" /Type /XObject\n")
appearance_buffer.WriteString(" /Subtype /Form\n")
appearance_buffer.WriteString(fmt.Sprintf(" /BBox [0 0 %f %f]\n", rectWidth, rectHeight))
appearance_buffer.WriteString(" /Matrix [1 0 0 1 0 0]\n") // No scaling or translation
// Resources dictionary
appearance_buffer.WriteString(" /Resources <<\n")
appearance_buffer.WriteString(" /Font <<\n")
appearance_buffer.WriteString(" /F1 <<\n")
appearance_buffer.WriteString(" /Type /Font\n")
appearance_buffer.WriteString(" /Subtype /Type1\n")
appearance_buffer.WriteString(" /BaseFont /Times-Roman\n")
appearance_buffer.WriteString(" >>\n")
appearance_buffer.WriteString(" >>\n")
appearance_buffer.WriteString(" >>\n")
appearance_buffer.WriteString(" /FormType 1\n")
appearance_buffer.WriteString(fmt.Sprintf(" /Length %d\n", appearance_stream_buffer.Len()))
appearance_buffer.WriteString(">>\n")
appearance_buffer.WriteString("stream\n")
appearance_buffer.Write(appearance_stream_buffer.Bytes())
appearance_buffer.WriteString("endstream\n")
return appearance_buffer.Bytes(), nil
}

View File

@@ -10,7 +10,7 @@ func (context *SignContext) createCatalog() ([]byte, error) {
// Start the catalog object // Start the catalog object
catalog_buffer.WriteString("<<\n") catalog_buffer.WriteString("<<\n")
catalog_buffer.WriteString(" /Type /Catalog") catalog_buffer.WriteString(" /Type /Catalog\n")
// (Optional; PDF 1.4) The version of the PDF specification to which // (Optional; PDF 1.4) The version of the PDF specification to which
// the document conforms (for example, 1.4) if later than the version // the document conforms (for example, 1.4) if later than the version
@@ -49,33 +49,45 @@ func (context *SignContext) createCatalog() ([]byte, error) {
// Add Pages and Names references if they exist // Add Pages and Names references if they exist
if foundPages { if foundPages {
pages := root.Key("Pages").GetPtr() pages := root.Key("Pages").GetPtr()
catalog_buffer.WriteString(" /Pages " + strconv.Itoa(int(pages.GetID())) + " " + strconv.Itoa(int(pages.GetGen())) + " R") catalog_buffer.WriteString(" /Pages " + strconv.Itoa(int(pages.GetID())) + " " + strconv.Itoa(int(pages.GetGen())) + " R\n")
} }
if foundNames { if foundNames {
names := root.Key("Names").GetPtr() names := root.Key("Names").GetPtr()
catalog_buffer.WriteString(" /Names " + strconv.Itoa(int(names.GetID())) + " " + strconv.Itoa(int(names.GetGen())) + " R") catalog_buffer.WriteString(" /Names " + strconv.Itoa(int(names.GetID())) + " " + strconv.Itoa(int(names.GetGen())) + " R\n")
} }
// Start the AcroForm dictionary with /NeedAppearances // Start the AcroForm dictionary with /NeedAppearances
catalog_buffer.WriteString(" /AcroForm << /Fields [") catalog_buffer.WriteString(" /AcroForm <<\n")
catalog_buffer.WriteString(" /Fields [")
// Add existing signatures to the AcroForm dictionary // Add existing signatures to the AcroForm dictionary
for i, sig := range context.SignData.ExistingSignatures { for i, sig := range context.existingSignatures {
if i > 0 { if i > 0 {
catalog_buffer.WriteString(" ") catalog_buffer.WriteString(" ")
} }
catalog_buffer.WriteString(strconv.Itoa(int(sig.ObjectId)) + " 0 R") catalog_buffer.WriteString(strconv.Itoa(int(sig.objectId)) + " 0 R")
} }
// Add the visual signature field to the AcroForm dictionary // Add the visual signature field to the AcroForm dictionary
if len(context.SignData.ExistingSignatures) > 0 { if len(context.existingSignatures) > 0 {
catalog_buffer.WriteString(" ") 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("]") // close Fields array catalog_buffer.WriteString("]\n") // close Fields array
catalog_buffer.WriteString(" /NeedAppearances false") // (Optional; deprecated in PDF 2.0) A flag specifying whether
// to construct appearance streams and appearance
// dictionaries for all widget annotations in the document (see
// 12.7.4.3, "Variable text"). Default value: false. A PDF writer
// shall include this key, with a value of true, if it has not
// provided appearance streams for all visible widget
// annotations present in the document.
// if context.SignData.Visible {
// catalog_buffer.WriteString(" /NeedAppearances true")
// } else {
// catalog_buffer.WriteString(" /NeedAppearances false")
// }
// Signature flags (Table 225) // Signature flags (Table 225)
// //
@@ -100,14 +112,14 @@ func (context *SignContext) createCatalog() ([]byte, error) {
// Set SigFlags and Permissions based on Signature Type // Set SigFlags and Permissions based on Signature Type
switch context.SignData.Signature.CertType { switch context.SignData.Signature.CertType {
case CertificationSignature, ApprovalSignature, TimeStampSignature: case CertificationSignature, ApprovalSignature, TimeStampSignature:
catalog_buffer.WriteString(" /SigFlags 3") catalog_buffer.WriteString(" /SigFlags 3\n")
case UsageRightsSignature: case UsageRightsSignature:
catalog_buffer.WriteString(" /SigFlags 1") catalog_buffer.WriteString(" /SigFlags 1\n")
} }
// Finalize the AcroForm and Catalog object // Finalize the AcroForm and Catalog object
catalog_buffer.WriteString(" >>\n") // Close AcroForm catalog_buffer.WriteString(" >>\n") // Close AcroForm
catalog_buffer.WriteString(">>\n") // Close Catalog catalog_buffer.WriteString(">>\n") // Close Catalog
return catalog_buffer.Bytes(), nil return catalog_buffer.Bytes(), nil
} }

View File

@@ -15,17 +15,17 @@ var testFiles = []struct {
{ {
file: "../testfiles/testfile20.pdf", file: "../testfiles/testfile20.pdf",
expectedCatalogs: map[CertType]string{ expectedCatalogs: map[CertType]string{
CertificationSignature: "<<\n /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n", CertificationSignature: "<<\n /Type /Catalog\n /Pages 3 0 R\n /AcroForm <<\n /Fields [10 0 R]\n /SigFlags 3\n >>\n>>\n",
UsageRightsSignature: "<<\n /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 1 >>\n>>\n", UsageRightsSignature: "<<\n /Type /Catalog\n /Pages 3 0 R\n /AcroForm <<\n /Fields [10 0 R]\n /SigFlags 1\n >>\n>>\n",
ApprovalSignature: "<<\n /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n", ApprovalSignature: "<<\n /Type /Catalog\n /Pages 3 0 R\n /AcroForm <<\n /Fields [10 0 R]\n /SigFlags 3\n >>\n>>\n",
}, },
}, },
{ {
file: "../testfiles/testfile21.pdf", file: "../testfiles/testfile21.pdf",
expectedCatalogs: map[CertType]string{ expectedCatalogs: map[CertType]string{
CertificationSignature: "<<\n /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n", CertificationSignature: "<<\n /Type /Catalog\n /Pages 9 0 R\n /Names 6 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 3\n >>\n>>\n",
UsageRightsSignature: "<<\n /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 1 >>\n>>\n", UsageRightsSignature: "<<\n /Type /Catalog\n /Pages 9 0 R\n /Names 6 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 1\n >>\n>>\n",
ApprovalSignature: "<<\n /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n", ApprovalSignature: "<<\n /Type /Catalog\n /Pages 9 0 R\n /Names 6 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 3\n >>\n>>\n",
}, },
}, },
} }
@@ -33,7 +33,7 @@ var testFiles = []struct {
func TestCreateCatalog(t *testing.T) { func TestCreateCatalog(t *testing.T) {
for _, testFile := range testFiles { for _, testFile := range testFiles {
for certType, expectedCatalog := range testFile.expectedCatalogs { for certType, expectedCatalog := range testFile.expectedCatalogs {
t.Run(fmt.Sprintf("%s_certType-%d", testFile.file, certType), func(st *testing.T) { t.Run(fmt.Sprintf("%s_%s", testFile.file, certType.String()), func(st *testing.T) {
inputFile, err := os.Open(testFile.file) inputFile, err := os.Open(testFile.file)
if err != nil { if err != nil {
st.Errorf("Failed to load test PDF") st.Errorf("Failed to load test PDF")
@@ -57,13 +57,7 @@ func TestCreateCatalog(t *testing.T) {
PDFReader: rdr, PDFReader: rdr,
InputFile: inputFile, InputFile: inputFile,
VisualSignData: VisualSignData{ VisualSignData: VisualSignData{
ObjectId: uint32(rdr.XrefInformation.ItemCount), objectId: uint32(rdr.XrefInformation.ItemCount),
},
CatalogData: CatalogData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
},
InfoData: InfoData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
}, },
SignData: SignData{ SignData: SignData{
Signature: SignDataSignature{ Signature: SignDataSignature{
@@ -80,7 +74,7 @@ func TestCreateCatalog(t *testing.T) {
} }
if string(catalog) != expectedCatalog { if string(catalog) != expectedCatalog {
st.Errorf("Catalog mismatch, expected\n%s\nbut got\n%s", expectedCatalog, catalog) st.Errorf("Catalog mismatch, expected\n%q\nbut got\n%q", expectedCatalog, catalog)
} }
}) })
} }

View File

@@ -176,7 +176,7 @@ func (context *SignContext) createSignaturePlaceholder() []byte {
// //
// A timestamp can be embedded in a CMS binary data object (see 12.8.3.3, "CMS // A timestamp can be embedded in a CMS binary data object (see 12.8.3.3, "CMS
// (PKCS #7) signatures"). // (PKCS #7) signatures").
if context.SignData.TSA.URL == "" { if context.SignData.TSA.URL == "" && !context.SignData.Signature.Info.Date.IsZero() {
signature_buffer.WriteString(" /M ") signature_buffer.WriteString(" /M ")
signature_buffer.WriteString(pdfDateTime(context.SignData.Signature.Info.Date)) signature_buffer.WriteString(pdfDateTime(context.SignData.Signature.Info.Date))
signature_buffer.WriteString("\n") signature_buffer.WriteString("\n")
@@ -499,7 +499,7 @@ func (context *SignContext) fetchExistingSignatures() ([]SignData, error) {
if field.Key("FT").Name() == "Sig" { if field.Key("FT").Name() == "Sig" {
ptr := field.GetPtr() ptr := field.GetPtr()
sig := SignData{ sig := SignData{
ObjectId: uint32(ptr.GetID()), objectId: uint32(ptr.GetID()),
} }
signatures = append(signatures, sig) signatures = append(signatures, sig)
} }

View File

@@ -63,21 +63,12 @@ func TestCreateSignaturePlaceholder(t *testing.T) {
}, },
} }
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3 sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 3
context := SignContext{ context := SignContext{
PDFReader: rdr, PDFReader: rdr,
InputFile: inputFile, InputFile: inputFile,
VisualSignData: VisualSignData{ SignData: sign_data,
ObjectId: uint32(rdr.XrefInformation.ItemCount),
},
CatalogData: CatalogData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
},
InfoData: InfoData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
},
SignData: sign_data,
} }
signature := context.createSignaturePlaceholder() signature := context.createSignaturePlaceholder()

View File

@@ -27,20 +27,37 @@ const (
// pageNumber: the page number where the signature should be placed. // pageNumber: the page number where the signature should be placed.
// rect: the rectangle defining the position and size of the signature field. // rect: the rectangle defining the position and size of the signature field.
// Returns the visual signature string and an error if any. // Returns the visual signature string and an error if any.
func (context *SignContext) createVisualSignature(visible bool, pageNumber int, rect [4]float64) ([]byte, error) { func (context *SignContext) createVisualSignature(visible bool, pageNumber uint32, rect [4]float64) ([]byte, error) {
var visual_signature bytes.Buffer var visual_signature bytes.Buffer
visual_signature.WriteString("<<\n")
// Define the object as an annotation. // Define the object as an annotation.
visual_signature.WriteString("<< /Type /Annot") visual_signature.WriteString(" /Type /Annot\n")
// Specify the annotation subtype as a widget. // Specify the annotation subtype as a widget.
visual_signature.WriteString(" /Subtype /Widget") visual_signature.WriteString(" /Subtype /Widget\n")
if visible { if visible {
// Set the position and size of the signature field if visible. // Set the position and size of the signature field if visible.
visual_signature.WriteString(fmt.Sprintf(" /Rect [%f %f %f %f]", rect[0], rect[1], rect[2], rect[3])) visual_signature.WriteString(fmt.Sprintf(" /Rect [%f %f %f %f]\n", rect[0], rect[1], rect[2], rect[3]))
appearance, err := context.createAppearance(rect)
if err != nil {
return nil, fmt.Errorf("failed to create appearance: %w", err)
}
appearanceObjectId, err := context.addObject(appearance)
if err != nil {
return nil, fmt.Errorf("failed to add appearance object: %w", err)
}
// An appearance dictionary specifying how the annotation
// shall be presented visually on the page (see 12.5.5, "Appearance streams").
visual_signature.WriteString(fmt.Sprintf(" /AP << /N %d 0 R >>\n", appearanceObjectId))
} else { } else {
// Set the rectangle to zero if the signature is invisible. // Set the rectangle to zero if the signature is invisible.
visual_signature.WriteString(" /Rect [0 0 0 0]") visual_signature.WriteString(" /Rect [0 0 0 0]\n")
} }
// Retrieve the root object from the PDF trailer. // Retrieve the root object from the PDF trailer.
@@ -72,40 +89,72 @@ func (context *SignContext) createVisualSignature(visible bool, pageNumber int,
page_ptr := page.GetPtr() page_ptr := page.GetPtr()
// Store the page ID in the visual signature context so that we can add it to xref table later. // Store the page ID in the visual signature context so that we can add it to xref table later.
context.VisualSignData.PageId = page_ptr.GetID() context.VisualSignData.pageObjectId = page_ptr.GetID()
// Add the page reference to the visual signature. // Add the page reference to the visual signature.
visual_signature.WriteString(" /P " + strconv.Itoa(int(page_ptr.GetID())) + " " + strconv.Itoa(int(page_ptr.GetGen())) + " R") visual_signature.WriteString(" /P " + strconv.Itoa(int(page_ptr.GetID())) + " " + strconv.Itoa(int(page_ptr.GetGen())) + " R\n")
} }
// Define the annotation flags for the signature field (132) // Define the annotation flags for the signature field (132)
// annotationFlags := AnnotationFlagPrint | AnnotationFlagNoZoom | AnnotationFlagNoRotate | AnnotationFlagReadOnly | AnnotationFlagLockedContents annotationFlags := AnnotationFlagPrint | AnnotationFlagLocked
visual_signature.WriteString(fmt.Sprintf(" /F %d", 132)) visual_signature.WriteString(fmt.Sprintf(" /F %d\n", annotationFlags))
// Define the field type as a signature.
visual_signature.WriteString(" /FT /Sig")
// Set a unique title for the signature field.
visual_signature.WriteString(" /T " + pdfString("Signature "+strconv.Itoa(len(context.SignData.ExistingSignatures)+1)))
// (Optional) A set of bit flags specifying the interpretation of specific entries // Define the field type as a signature.
// in this dictionary. A value of 1 for the flag indicates that the associated entry visual_signature.WriteString(" /FT /Sig\n")
// is a required constraint. A value of 0 indicates that the associated entry is // Set a unique title for the signature field.
// an optional constraint. Bit positions are 1 (Filter); 2 (SubFilter); 3 (V); 4 visual_signature.WriteString(fmt.Sprintf(" /T %s\n", pdfString("Signature "+strconv.Itoa(len(context.existingSignatures)+1))))
// (Reasons); 5 (LegalAttestation); 6 (AddRevInfo); and 7 (DigestMethod).
// For PDF 2.0 the following bit flags are added: 8 (Lockdocument); and 9
// (AppearanceFilter). Default value: 0.
visual_signature.WriteString(" /Ff 0")
// Reference the signature dictionary. // Reference the signature dictionary.
visual_signature.WriteString(" /V " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R") visual_signature.WriteString(fmt.Sprintf(" /V %d 0 R\n", context.SignData.objectId))
// Close the dictionary and end the object. // Close the dictionary and end the object.
visual_signature.WriteString(" >>\n") visual_signature.WriteString(">>\n")
return visual_signature.Bytes(), nil return visual_signature.Bytes(), nil
} }
func (context *SignContext) createIncPageUpdate(pageNumber, annot uint32) ([]byte, error) {
var page_buffer bytes.Buffer
// Retrieve the root object from the PDF trailer.
root := context.PDFReader.Trailer().Key("Root")
page, err := findPageByNumber(root.Key("Pages"), pageNumber)
if err != nil {
return nil, err
}
page_buffer.WriteString("<<\n")
// TODO: Update digitorus/pdf to get raw values without resolving pointers
for _, key := range page.Keys() {
switch key {
case "Contents", "Parent":
ptr := page.Key(key).GetPtr()
page_buffer.WriteString(fmt.Sprintf(" /%s %d 0 R\n", key, ptr.GetID()))
case "Annots":
page_buffer.WriteString(" /Annots [\n")
for i := 0; i < page.Key("Annots").Len(); i++ {
ptr := page.Key(key).Index(i).GetPtr()
page_buffer.WriteString(fmt.Sprintf(" %d 0 R\n", ptr.GetID()))
}
page_buffer.WriteString(fmt.Sprintf(" %d 0 R\n", annot))
page_buffer.WriteString(" ]\n")
default:
page_buffer.WriteString(fmt.Sprintf(" /%s %s\n", key, page.Key(key).String()))
}
}
if page.Key("Annots").IsNull() {
page_buffer.WriteString(fmt.Sprintf(" /Annots [%d 0 R]\n", annot))
}
page_buffer.WriteString(">>\n")
return page_buffer.Bytes(), nil
}
// Helper function to find a page by its number. // Helper function to find a page by its number.
func findPageByNumber(pages pdf.Value, pageNumber int) (pdf.Value, error) { func findPageByNumber(pages pdf.Value, pageNumber uint32) (pdf.Value, error) {
if pages.Key("Type").Name() == "Pages" { if pages.Key("Type").Name() == "Pages" {
kids := pages.Key("Kids") kids := pages.Key("Kids")
for i := 0; i < kids.Len(); i++ { for i := 0; i < kids.Len(); i++ {

View File

@@ -45,24 +45,15 @@ func TestVisualSignature(t *testing.T) {
}, },
} }
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3 sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 3
context := SignContext{ context := SignContext{
PDFReader: rdr, PDFReader: rdr,
InputFile: input_file, InputFile: input_file,
VisualSignData: VisualSignData{ SignData: sign_data,
ObjectId: uint32(rdr.XrefInformation.ItemCount),
},
CatalogData: CatalogData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
},
InfoData: InfoData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
},
SignData: sign_data,
} }
expected_visual_signature := "<< /Type /Annot /Subtype /Widget /Rect [0 0 0 0] /P 4 0 R /F 132 /FT /Sig /T (Signature 1) /Ff 0 /V 13 0 R >>\n" expected_visual_signature := "<<\n /Type /Annot\n /Subtype /Widget\n /Rect [0 0 0 0]\n /P 4 0 R\n /F 132\n /FT /Sig\n /T (Signature 1)\n /V 13 0 R\n>>\n"
visual_signature, err := context.createVisualSignature(false, 1, [4]float64{0, 0, 0, 0}) visual_signature, err := context.createVisualSignature(false, 1, [4]float64{0, 0, 0, 0})
if err != nil { if err != nil {

View File

@@ -40,23 +40,46 @@ func (context *SignContext) addObject(object []byte) (uint32, error) {
Offset: int64(context.OutputBuffer.Buff.Len()) + 1, Offset: int64(context.OutputBuffer.Buff.Len()) + 1,
}) })
err := context.writeObject(objectID, object)
if err != nil {
return 0, fmt.Errorf("failed to write object: %w", err)
}
return objectID, nil
}
func (context *SignContext) updateObject(id uint32, object []byte) error {
context.updatedXrefEntries = append(context.updatedXrefEntries, xrefEntry{
ID: id,
Offset: int64(context.OutputBuffer.Buff.Len()) + 1,
})
err := context.writeObject(id, object)
if err != nil {
return fmt.Errorf("failed to write object: %w", err)
}
return nil
}
func (context *SignContext) writeObject(id uint32, object []byte) error {
// Write the object header // Write the object header
if _, err := context.OutputBuffer.Write([]byte(fmt.Sprintf("\n%d 0 obj\n", objectID))); err != nil { if _, err := context.OutputBuffer.Write([]byte(fmt.Sprintf("\n%d 0 obj\n", id))); err != nil {
return 0, fmt.Errorf("failed to write object header: %w", err) return fmt.Errorf("failed to write object header: %w", err)
} }
// Write the object content // Write the object content
object = bytes.TrimSpace(object) object = bytes.TrimSpace(object)
if _, err := context.OutputBuffer.Write(object); err != nil { if _, err := context.OutputBuffer.Write(object); err != nil {
return 0, fmt.Errorf("failed to write object content: %w", err) return fmt.Errorf("failed to write object content: %w", err)
} }
// Write the object footer // Write the object footer
if _, err := context.OutputBuffer.Write([]byte(objectFooter)); err != nil { if _, err := context.OutputBuffer.Write([]byte(objectFooter)); err != nil {
return 0, fmt.Errorf("failed to write object footer: %w", err) return fmt.Errorf("failed to write object footer: %w", err)
} }
return objectID, nil return nil
} }
// writeXref writes the cross-reference table or stream based on the PDF type. // writeXref writes the cross-reference table or stream based on the PDF type.
@@ -115,6 +138,19 @@ func (context *SignContext) writeIncrXrefTable() error {
return fmt.Errorf("failed to write incremental xref header: %w", err) return fmt.Errorf("failed to write incremental xref header: %w", err)
} }
// Write updated entries
for _, entry := range context.updatedXrefEntries {
pageXrefObj := fmt.Sprintf("%d %d\n", entry.ID, 1)
if _, err := context.OutputBuffer.Write([]byte(pageXrefObj)); err != nil {
return fmt.Errorf("failed to write updated xref object: %w", err)
}
xrefLine := fmt.Sprintf("%010d 00000 n\r\n", entry.Offset)
if _, err := context.OutputBuffer.Write([]byte(xrefLine)); err != nil {
return fmt.Errorf("failed to write updated incremental xref entry: %w", err)
}
}
// Write xref subsection header // Write xref subsection header
startXrefObj := fmt.Sprintf("%d %d\n", context.lastXrefID+1, len(context.newXrefEntries)) startXrefObj := fmt.Sprintf("%d %d\n", context.lastXrefID+1, len(context.newXrefEntries))
if _, err := context.OutputBuffer.Write([]byte(startXrefObj)); err != nil { if _, err := context.OutputBuffer.Write([]byte(startXrefObj)); err != nil {
@@ -190,26 +226,25 @@ func encodeXrefStream(data []byte, predictor int64) ([]byte, error) {
// writeXrefStreamHeader writes the header for the xref stream. // writeXrefStreamHeader writes the header for the xref stream.
func writeXrefStreamHeader(context *SignContext, streamLength int) error { func writeXrefStreamHeader(context *SignContext, streamLength int) error {
newRoot := fmt.Sprintf("Root %d 0 R", context.CatalogData.ObjectId)
id := context.PDFReader.Trailer().Key("ID") id := context.PDFReader.Trailer().Key("ID")
id0 := hex.EncodeToString([]byte(id.Index(0).RawString())) id0 := hex.EncodeToString([]byte(id.Index(0).RawString()))
id1 := hex.EncodeToString([]byte(id.Index(0).RawString())) id1 := hex.EncodeToString([]byte(id.Index(0).RawString()))
newXref := fmt.Sprintf("%d 0 obj\n<< /Type /XRef /Length %d /Filter /FlateDecode /DecodeParms << /Columns %d /Predictor %d >> /W [ 1 3 1 ] /Prev %d /Size %d /Index [ %d 4 ] /%s /ID [<%s><%s>] >>\n", var buffer bytes.Buffer
context.SignData.ObjectId+1, buffer.WriteString(fmt.Sprintf("%d 0 obj\n", context.SignData.objectId))
streamLength, buffer.WriteString("<< /Type /XRef\n")
xrefStreamColumns, buffer.WriteString(fmt.Sprintf(" /Length %d\n", streamLength))
xrefStreamPredictor, buffer.WriteString(" /Filter /FlateDecode\n")
context.PDFReader.XrefInformation.StartPos, buffer.WriteString(fmt.Sprintf(" /DecodeParms << /Columns %d /Predictor %d >>\n", xrefStreamColumns, xrefStreamPredictor))
context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries))+1, buffer.WriteString(" /W [ 1 3 1 ]\n")
context.PDFReader.XrefInformation.ItemCount, buffer.WriteString(fmt.Sprintf(" /Prev %d\n", context.PDFReader.XrefInformation.StartPos))
newRoot, buffer.WriteString(fmt.Sprintf(" /Size %d\n", context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries))+1))
id0, buffer.WriteString(fmt.Sprintf(" /Index [ %d 4 ]\n", context.PDFReader.XrefInformation.ItemCount))
id1, buffer.WriteString(fmt.Sprintf(" /Root %d 0 R\n", context.CatalogData.ObjectId))
) buffer.WriteString(fmt.Sprintf(" /ID [<%s><%s>]\n", id0, id1))
buffer.WriteString(">>\n")
_, err := io.WriteString(context.OutputBuffer, newXref) _, err := context.OutputBuffer.Write(buffer.Bytes())
return err return err
} }

View File

@@ -29,7 +29,6 @@ type TSA struct {
type RevocationFunction func(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error type RevocationFunction func(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error
type SignData struct { type SignData struct {
ObjectId uint32
Signature SignDataSignature Signature SignDataSignature
Signer crypto.Signer Signer crypto.Signer
DigestAlgorithm crypto.Hash DigestAlgorithm crypto.Hash
@@ -38,12 +37,24 @@ type SignData struct {
TSA TSA TSA TSA
RevocationData revocation.InfoArchival RevocationData revocation.InfoArchival
RevocationFunction RevocationFunction RevocationFunction RevocationFunction
ExistingSignatures []SignData Appearance Appearance
objectId uint32
}
// Appearance represents the appearance of the signature
type Appearance struct {
Visible bool
Page uint32
LowerLeftX float64
LowerLeftY float64
UpperRightX float64
UpperRightY float64
} }
type VisualSignData struct { type VisualSignData struct {
PageId uint32 pageObjectId uint32
ObjectId uint32 objectId uint32
} }
type InfoData struct { type InfoData struct {
@@ -97,8 +108,10 @@ type SignContext struct {
SignatureMaxLength uint32 SignatureMaxLength uint32
SignatureMaxLengthBase uint32 SignatureMaxLengthBase uint32
lastXrefID uint32 existingSignatures []SignData
newXrefEntries []xrefEntry lastXrefID uint32
newXrefEntries []xrefEntry
updatedXrefEntries []xrefEntry
} }
func SignFile(input string, output string, sign_data SignData) error { func SignFile(input string, output string, sign_data SignData) error {
@@ -129,21 +142,12 @@ func SignFile(input string, output string, sign_data SignData) error {
} }
func Sign(input io.ReadSeeker, output io.Writer, rdr *pdf.Reader, size int64, sign_data SignData) error { func Sign(input io.ReadSeeker, output io.Writer, rdr *pdf.Reader, size int64, sign_data SignData) error {
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 2 sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 2
context := SignContext{ context := SignContext{
PDFReader: rdr, PDFReader: rdr,
InputFile: input, InputFile: input,
OutputFile: output, OutputFile: output,
VisualSignData: VisualSignData{
ObjectId: uint32(rdr.XrefInformation.ItemCount),
},
CatalogData: CatalogData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
},
InfoData: InfoData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
},
SignData: sign_data, SignData: sign_data,
SignatureMaxLengthBase: uint32(hex.EncodedLen(512)), SignatureMaxLengthBase: uint32(hex.EncodedLen(512)),
} }
@@ -153,7 +157,7 @@ func Sign(input io.ReadSeeker, output io.Writer, rdr *pdf.Reader, size int64, si
if err != nil { if err != nil {
return err return err
} }
context.SignData.ExistingSignatures = existingSignatures context.existingSignatures = existingSignatures
err = context.SignPDF() err = context.SignPDF()
if err != nil { if err != nil {
@@ -174,6 +178,9 @@ func (context *SignContext) SignPDF() error {
if !context.SignData.DigestAlgorithm.Available() { if !context.SignData.DigestAlgorithm.Available() {
context.SignData.DigestAlgorithm = crypto.SHA256 context.SignData.DigestAlgorithm = crypto.SHA256
} }
if context.SignData.Appearance.Page == 0 {
context.SignData.Appearance.Page = 1
}
context.OutputBuffer = filebuffer.New([]byte{}) context.OutputBuffer = filebuffer.New([]byte{})
@@ -271,25 +278,49 @@ func (context *SignContext) SignPDF() error {
} }
// Write the new signature object // Write the new signature object
context.SignData.ObjectId, err = context.addObject(signature_object) context.SignData.objectId, err = context.addObject(signature_object)
if err != nil { if err != nil {
return fmt.Errorf("failed to add signature object: %w", err) return fmt.Errorf("failed to add signature object: %w", err)
} }
// Create visual signature (visible or invisible based on CertType) // Create visual signature (visible or invisible based on CertType)
// visible := context.SignData.Signature.CertType == CertificationSignature visible := false
rectangle := [4]float64{0, 0, 0, 0}
if context.SignData.Signature.CertType != ApprovalSignature && context.SignData.Appearance.Visible {
return fmt.Errorf("visible signatures are only allowed for approval signatures")
} else if context.SignData.Signature.CertType == ApprovalSignature && context.SignData.Appearance.Visible {
visible = true
rectangle = [4]float64{
context.SignData.Appearance.LowerLeftX,
context.SignData.Appearance.LowerLeftY,
context.SignData.Appearance.UpperRightX,
context.SignData.Appearance.UpperRightY,
}
}
// Example usage: passing page number and default rect values // Example usage: passing page number and default rect values
visual_signature, err := context.createVisualSignature(false, 1, [4]float64{0, 0, 0, 0}) visual_signature, err := context.createVisualSignature(visible, context.SignData.Appearance.Page, rectangle)
if err != nil { if err != nil {
return fmt.Errorf("failed to create visual signature: %w", err) return fmt.Errorf("failed to create visual signature: %w", err)
} }
// Write the new visual signature object. // Write the new visual signature object.
context.VisualSignData.ObjectId, err = context.addObject(visual_signature) context.VisualSignData.objectId, err = context.addObject(visual_signature)
if err != nil { if err != nil {
return fmt.Errorf("failed to add visual signature object: %w", err) return fmt.Errorf("failed to add visual signature object: %w", err)
} }
if context.SignData.Appearance.Visible {
inc_page_update, err := context.createIncPageUpdate(context.SignData.Appearance.Page, context.VisualSignData.objectId)
if err != nil {
return fmt.Errorf("failed to create incremental page update: %w", err)
}
err = context.updateObject(context.VisualSignData.pageObjectId, inc_page_update)
if err != nil {
return fmt.Errorf("failed to add incremental page update object: %w", err)
}
}
// Create a new catalog object // Create a new catalog object
catalog, err := context.createCatalog() catalog, err := context.createCatalog()
if err != nil { if err != nil {

View File

@@ -229,6 +229,54 @@ func TestSignPDFFileUTF8(t *testing.T) {
} }
} }
func TestSignPDFVisible(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)
inputFilePath := "../testfiles/minimal.pdf"
originalFileName := filepath.Base(inputFilePath)
tmpfile, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(tmpfile.Name())
}
err = SignFile(inputFilePath, tmpfile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "John Doe",
Location: "Somewhere",
Reason: "Test with visible signature",
ContactInfo: "None",
},
CertType: ApprovalSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
Appearance: Appearance{
Visible: true,
LowerLeftX: 350,
LowerLeftY: 75,
UpperRightX: 600,
UpperRightY: 100,
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("%s: %s", originalFileName, err.Error())
}
_, err = verify.File(tmpfile)
if err != nil {
t.Fatalf("%s: %s", tmpfile.Name(), err.Error())
if err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil {
t.Error(err)
}
}
}
func BenchmarkSignPDF(b *testing.B) { func BenchmarkSignPDF(b *testing.B) {
cert, pkey := loadCertificateAndKey(&testing.T{}) cert, pkey := loadCertificateAndKey(&testing.T{})
certificateChains := [][]*x509.Certificate{} certificateChains := [][]*x509.Certificate{}