diff --git a/sign/appearance.go b/sign/appearance.go new file mode 100644 index 0000000..3a349fd --- /dev/null +++ b/sign/appearance.go @@ -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 +} diff --git a/sign/pdfcatalog.go b/sign/pdfcatalog.go index ee42bef..8182a70 100644 --- a/sign/pdfcatalog.go +++ b/sign/pdfcatalog.go @@ -10,7 +10,7 @@ func (context *SignContext) createCatalog() ([]byte, error) { // Start the catalog object 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 // 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 if foundPages { 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 { 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 - catalog_buffer.WriteString(" /AcroForm << /Fields [") + catalog_buffer.WriteString(" /AcroForm <<\n") + catalog_buffer.WriteString(" /Fields [") // Add existing signatures to the AcroForm dictionary - for i, sig := range context.SignData.ExistingSignatures { + for i, sig := range context.existingSignatures { if i > 0 { 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 - if len(context.SignData.ExistingSignatures) > 0 { + if len(context.existingSignatures) > 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("]") // 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) // @@ -100,14 +112,14 @@ func (context *SignContext) createCatalog() ([]byte, error) { // Set SigFlags and Permissions based on Signature Type switch context.SignData.Signature.CertType { case CertificationSignature, ApprovalSignature, TimeStampSignature: - catalog_buffer.WriteString(" /SigFlags 3") + catalog_buffer.WriteString(" /SigFlags 3\n") case UsageRightsSignature: - catalog_buffer.WriteString(" /SigFlags 1") + catalog_buffer.WriteString(" /SigFlags 1\n") } // Finalize the AcroForm and Catalog object - catalog_buffer.WriteString(" >>\n") // Close AcroForm - catalog_buffer.WriteString(">>\n") // Close Catalog + catalog_buffer.WriteString(" >>\n") // Close AcroForm + catalog_buffer.WriteString(">>\n") // Close Catalog return catalog_buffer.Bytes(), nil } diff --git a/sign/pdfcatalog_test.go b/sign/pdfcatalog_test.go index 5e68f01..c4e25e6 100644 --- a/sign/pdfcatalog_test.go +++ b/sign/pdfcatalog_test.go @@ -15,17 +15,17 @@ var testFiles = []struct { { file: "../testfiles/testfile20.pdf", expectedCatalogs: map[CertType]string{ - CertificationSignature: "<<\n /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n", - UsageRightsSignature: "<<\n /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 1 >>\n>>\n", - ApprovalSignature: "<<\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\n /Pages 3 0 R\n /AcroForm <<\n /Fields [10 0 R]\n /SigFlags 1\n >>\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", 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", - UsageRightsSignature: "<<\n /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 1 >>\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", + 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\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\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) { for _, testFile := range testFiles { 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) if err != nil { st.Errorf("Failed to load test PDF") @@ -57,13 +57,7 @@ func TestCreateCatalog(t *testing.T) { PDFReader: rdr, InputFile: inputFile, VisualSignData: VisualSignData{ - ObjectId: uint32(rdr.XrefInformation.ItemCount), - }, - CatalogData: CatalogData{ - ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1, - }, - InfoData: InfoData{ - ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2, + objectId: uint32(rdr.XrefInformation.ItemCount), }, SignData: SignData{ Signature: SignDataSignature{ @@ -80,7 +74,7 @@ func TestCreateCatalog(t *testing.T) { } 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) } }) } diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index 30a298c..0206026 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -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 // (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(pdfDateTime(context.SignData.Signature.Info.Date)) signature_buffer.WriteString("\n") @@ -499,7 +499,7 @@ func (context *SignContext) fetchExistingSignatures() ([]SignData, error) { if field.Key("FT").Name() == "Sig" { ptr := field.GetPtr() sig := SignData{ - ObjectId: uint32(ptr.GetID()), + objectId: uint32(ptr.GetID()), } signatures = append(signatures, sig) } diff --git a/sign/pdfsignature_test.go b/sign/pdfsignature_test.go index 7aa31ed..83a10d9 100644 --- a/sign/pdfsignature_test.go +++ b/sign/pdfsignature_test.go @@ -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{ PDFReader: rdr, InputFile: inputFile, - 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, } signature := context.createSignaturePlaceholder() diff --git a/sign/pdfvisualsignature.go b/sign/pdfvisualsignature.go index 2621b79..6e9e957 100644 --- a/sign/pdfvisualsignature.go +++ b/sign/pdfvisualsignature.go @@ -27,20 +27,37 @@ const ( // pageNumber: the page number where the signature should be placed. // rect: the rectangle defining the position and size of the signature field. // 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 + visual_signature.WriteString("<<\n") + // Define the object as an annotation. - visual_signature.WriteString("<< /Type /Annot") + visual_signature.WriteString(" /Type /Annot\n") // Specify the annotation subtype as a widget. - visual_signature.WriteString(" /Subtype /Widget") + visual_signature.WriteString(" /Subtype /Widget\n") 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 { // 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. @@ -72,40 +89,72 @@ func (context *SignContext) createVisualSignature(visible bool, pageNumber int, page_ptr := page.GetPtr() // 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. - 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) - // annotationFlags := AnnotationFlagPrint | AnnotationFlagNoZoom | AnnotationFlagNoRotate | AnnotationFlagReadOnly | AnnotationFlagLockedContents - visual_signature.WriteString(fmt.Sprintf(" /F %d", 132)) + annotationFlags := AnnotationFlagPrint | AnnotationFlagLocked + visual_signature.WriteString(fmt.Sprintf(" /F %d\n", annotationFlags)) + // Define the field type as a signature. - visual_signature.WriteString(" /FT /Sig") + visual_signature.WriteString(" /FT /Sig\n") // 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 - // in this dictionary. A value of 1 for the flag indicates that the associated entry - // is a required constraint. A value of 0 indicates that the associated entry is - // an optional constraint. Bit positions are 1 (Filter); 2 (SubFilter); 3 (V); 4 - // (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") + visual_signature.WriteString(fmt.Sprintf(" /T %s\n", pdfString("Signature "+strconv.Itoa(len(context.existingSignatures)+1)))) // 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. - visual_signature.WriteString(" >>\n") + visual_signature.WriteString(">>\n") 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. -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" { kids := pages.Key("Kids") for i := 0; i < kids.Len(); i++ { diff --git a/sign/pdfvisualsignature_test.go b/sign/pdfvisualsignature_test.go index 6b2037c..d80cf2c 100644 --- a/sign/pdfvisualsignature_test.go +++ b/sign/pdfvisualsignature_test.go @@ -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{ PDFReader: rdr, InputFile: input_file, - 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, } - 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}) if err != nil { diff --git a/sign/pdfxref.go b/sign/pdfxref.go index ba61225..6be738d 100644 --- a/sign/pdfxref.go +++ b/sign/pdfxref.go @@ -40,23 +40,46 @@ func (context *SignContext) addObject(object []byte) (uint32, error) { 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 - if _, err := context.OutputBuffer.Write([]byte(fmt.Sprintf("\n%d 0 obj\n", objectID))); err != nil { - return 0, fmt.Errorf("failed to write object header: %w", err) + if _, err := context.OutputBuffer.Write([]byte(fmt.Sprintf("\n%d 0 obj\n", id))); err != nil { + return fmt.Errorf("failed to write object header: %w", err) } // Write the object content object = bytes.TrimSpace(object) 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 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. @@ -115,6 +138,19 @@ func (context *SignContext) writeIncrXrefTable() error { 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 startXrefObj := fmt.Sprintf("%d %d\n", context.lastXrefID+1, len(context.newXrefEntries)) 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. func writeXrefStreamHeader(context *SignContext, streamLength int) error { - newRoot := fmt.Sprintf("Root %d 0 R", context.CatalogData.ObjectId) - id := context.PDFReader.Trailer().Key("ID") id0 := 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", - context.SignData.ObjectId+1, - streamLength, - xrefStreamColumns, - xrefStreamPredictor, - context.PDFReader.XrefInformation.StartPos, - context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries))+1, - context.PDFReader.XrefInformation.ItemCount, - newRoot, - id0, - id1, - ) - - _, err := io.WriteString(context.OutputBuffer, newXref) + var buffer bytes.Buffer + buffer.WriteString(fmt.Sprintf("%d 0 obj\n", context.SignData.objectId)) + buffer.WriteString("<< /Type /XRef\n") + buffer.WriteString(fmt.Sprintf(" /Length %d\n", streamLength)) + buffer.WriteString(" /Filter /FlateDecode\n") + buffer.WriteString(fmt.Sprintf(" /DecodeParms << /Columns %d /Predictor %d >>\n", xrefStreamColumns, xrefStreamPredictor)) + buffer.WriteString(" /W [ 1 3 1 ]\n") + buffer.WriteString(fmt.Sprintf(" /Prev %d\n", context.PDFReader.XrefInformation.StartPos)) + buffer.WriteString(fmt.Sprintf(" /Size %d\n", context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries))+1)) + buffer.WriteString(fmt.Sprintf(" /Index [ %d 4 ]\n", context.PDFReader.XrefInformation.ItemCount)) + 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 := context.OutputBuffer.Write(buffer.Bytes()) return err } diff --git a/sign/sign.go b/sign/sign.go index ed17796..292a46d 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -29,7 +29,6 @@ type TSA struct { type RevocationFunction func(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error type SignData struct { - ObjectId uint32 Signature SignDataSignature Signer crypto.Signer DigestAlgorithm crypto.Hash @@ -38,12 +37,24 @@ type SignData struct { TSA TSA RevocationData revocation.InfoArchival 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 { - PageId uint32 - ObjectId uint32 + pageObjectId uint32 + objectId uint32 } type InfoData struct { @@ -97,8 +108,10 @@ type SignContext struct { SignatureMaxLength uint32 SignatureMaxLengthBase uint32 - lastXrefID uint32 - newXrefEntries []xrefEntry + existingSignatures []SignData + lastXrefID uint32 + newXrefEntries []xrefEntry + updatedXrefEntries []xrefEntry } 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 { - sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 2 + sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 2 context := SignContext{ - PDFReader: rdr, - InputFile: input, - 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, - }, + PDFReader: rdr, + InputFile: input, + OutputFile: output, SignData: sign_data, 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 { return err } - context.SignData.ExistingSignatures = existingSignatures + context.existingSignatures = existingSignatures err = context.SignPDF() if err != nil { @@ -174,6 +178,9 @@ func (context *SignContext) SignPDF() error { if !context.SignData.DigestAlgorithm.Available() { context.SignData.DigestAlgorithm = crypto.SHA256 } + if context.SignData.Appearance.Page == 0 { + context.SignData.Appearance.Page = 1 + } context.OutputBuffer = filebuffer.New([]byte{}) @@ -271,25 +278,49 @@ func (context *SignContext) SignPDF() error { } // Write the new signature object - context.SignData.ObjectId, err = context.addObject(signature_object) + context.SignData.objectId, err = context.addObject(signature_object) if err != nil { return fmt.Errorf("failed to add signature object: %w", err) } // 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 - 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 { return fmt.Errorf("failed to create visual signature: %w", err) } // 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 { 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 catalog, err := context.createCatalog() if err != nil { diff --git a/sign/sign_test.go b/sign/sign_test.go index 5055dd1..5e53cc0 100644 --- a/sign/sign_test.go +++ b/sign/sign_test.go @@ -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) { cert, pkey := loadCertificateAndKey(&testing.T{}) certificateChains := [][]*x509.Certificate{}