From e5cdb61bea12d268d0b6a86370c32771ff8562fc Mon Sep 17 00:00:00 2001 From: Paul van Brouwershaven Date: Wed, 11 Dec 2024 16:24:26 +0100 Subject: [PATCH] Refactor object handling --- sign/pdfbyterange.go | 60 ++++++------ sign/pdfcatalog.go | 40 ++++---- sign/pdfcatalog_test.go | 17 ++-- sign/pdfsignature.go | 69 ++++++++----- sign/pdfsignature_test.go | 165 +++++++++++++++----------------- sign/pdftrailer.go | 2 +- sign/pdfvisualsignature.go | 34 +++---- sign/pdfvisualsignature_test.go | 7 +- sign/pdfxref.go | 139 ++++++++++++--------------- sign/sign.go | 103 +++++++++----------- sign/sign_test.go | 2 + 11 files changed, 311 insertions(+), 327 deletions(-) diff --git a/sign/pdfbyterange.go b/sign/pdfbyterange.go index 03b66fd..f72b25b 100644 --- a/sign/pdfbyterange.go +++ b/sign/pdfbyterange.go @@ -1,6 +1,7 @@ package sign import ( + "bytes" "fmt" "strings" ) @@ -9,43 +10,46 @@ func (context *SignContext) updateByteRange() error { if _, err := context.OutputBuffer.Seek(0, 0); err != nil { return err } - output_file_size := int64(context.OutputBuffer.Buff.Len()) - // Calculate ByteRange values to replace them. - context.ByteRangeValues = make([]int64, 4) + // Set ByteRangeValues by looking for the /Contents< filled with zeros + contentsPlaceholder := bytes.Repeat([]byte("0"), int(context.SignatureMaxLength)) + contentsIndex := bytes.Index(context.OutputBuffer.Buff.Bytes(), contentsPlaceholder) + if contentsIndex == -1 { + return fmt.Errorf("failed to find contents placeholder") + } - // Signature ByteRange part 1 start byte is always byte 0. - context.ByteRangeValues[0] = int64(0) - - // Signature ByteRange part 1 length always stops at the actual signature start byte. - context.ByteRangeValues[1] = context.SignatureContentsStartByte - 1 - - // Signature ByteRange part 2 start byte directly starts after the actual signature. - context.ByteRangeValues[2] = context.ByteRangeValues[1] + 1 + int64(context.SignatureMaxLength) + 1 - - // Signature ByteRange part 2 length is everything else of the file. - context.ByteRangeValues[3] = output_file_size - context.ByteRangeValues[2] + // Calculate ByteRangeValues + signatureContentsStart := int64(contentsIndex) - 1 + signatureContentsEnd := signatureContentsStart + int64(context.SignatureMaxLength) + 2 + context.ByteRangeValues = []int64{ + 0, + signatureContentsStart, + signatureContentsEnd, + int64(context.OutputBuffer.Buff.Len()) - signatureContentsEnd, + } new_byte_range := fmt.Sprintf("/ByteRange [%d %d %d %d]", context.ByteRangeValues[0], context.ByteRangeValues[1], context.ByteRangeValues[2], context.ByteRangeValues[3]) - // Make sure our ByteRange string didn't shrink in length. - new_byte_range += strings.Repeat(" ", len(signatureByteRangePlaceholder)-len(new_byte_range)) - - if _, err := context.OutputBuffer.Seek(0, 0); err != nil { - return err - } - file_content := context.OutputBuffer.Buff.Bytes() - - if _, err := context.OutputBuffer.Write(file_content[:context.ByteRangeStartByte]); err != nil { - return err + // Make sure our ByteRange string has the same length as the placeholder. + if len(new_byte_range) < len(signatureByteRangePlaceholder) { + new_byte_range += strings.Repeat(" ", len(signatureByteRangePlaceholder)-len(new_byte_range)) + } else if len(new_byte_range) != len(signatureByteRangePlaceholder) { + return fmt.Errorf("new byte range string is the same lenght as the placeholder") } - // Write new ByteRange. - if _, err := context.OutputBuffer.Write([]byte(new_byte_range)); err != nil { - return err + // Find the placeholder in the buffer + placeholderIndex := bytes.Index(context.OutputBuffer.Buff.Bytes(), []byte(signatureByteRangePlaceholder)) + if placeholderIndex == -1 { + return fmt.Errorf("failed to find ByteRange placeholder") } - if _, err := context.OutputBuffer.Write(file_content[context.ByteRangeStartByte+int64(len(new_byte_range)):]); err != nil { + // Replace the placeholder with the new byte range + bufferBytes := context.OutputBuffer.Buff.Bytes() + copy(bufferBytes[placeholderIndex:placeholderIndex+len(new_byte_range)], []byte(new_byte_range)) + + // Rewrite the buffer with the updated bytes + context.OutputBuffer.Buff.Reset() + if _, err := context.OutputBuffer.Buff.Write(bufferBytes); err != nil { return err } diff --git a/sign/pdfcatalog.go b/sign/pdfcatalog.go index 2c90dcc..ee42bef 100644 --- a/sign/pdfcatalog.go +++ b/sign/pdfcatalog.go @@ -1,16 +1,16 @@ package sign import ( + "bytes" "strconv" - "strings" ) -func (context *SignContext) createCatalog() (string, error) { - var catalogBuilder strings.Builder +func (context *SignContext) createCatalog() ([]byte, error) { + var catalog_buffer bytes.Buffer // Start the catalog object - catalogBuilder.WriteString(strconv.Itoa(int(context.CatalogData.ObjectId)) + " 0 obj\n") - catalogBuilder.WriteString("<< /Type /Catalog") + catalog_buffer.WriteString("<<\n") + catalog_buffer.WriteString(" /Type /Catalog") // (Optional; PDF 1.4) The version of the PDF specification to which // the document conforms (for example, 1.4) if later than the version @@ -25,7 +25,7 @@ func (context *SignContext) createCatalog() (string, error) { // // If an incremental upgrade requires a version that is higher than specified by the document. // if context.PDFReader.PDFVersion < "2.0" { - // catalogBuilder.WriteString(" /Version /2.0") + // catalog_buffer.WriteString(" /Version /2.0") // } // Retrieve the root and check for necessary keys in one loop @@ -49,33 +49,33 @@ func (context *SignContext) createCatalog() (string, error) { // Add Pages and Names references if they exist if foundPages { pages := root.Key("Pages").GetPtr() - catalogBuilder.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") } if foundNames { names := root.Key("Names").GetPtr() - catalogBuilder.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") } // Start the AcroForm dictionary with /NeedAppearances - catalogBuilder.WriteString(" /AcroForm << /Fields [") + catalog_buffer.WriteString(" /AcroForm << /Fields [") // Add existing signatures to the AcroForm dictionary for i, sig := range context.SignData.ExistingSignatures { if i > 0 { - catalogBuilder.WriteString(" ") + catalog_buffer.WriteString(" ") } - catalogBuilder.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 { - catalogBuilder.WriteString(" ") + catalog_buffer.WriteString(" ") } - catalogBuilder.WriteString(strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 R") + catalog_buffer.WriteString(strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 R") - catalogBuilder.WriteString("]") // close Fields array + catalog_buffer.WriteString("]") // close Fields array - catalogBuilder.WriteString(" /NeedAppearances false") + catalog_buffer.WriteString(" /NeedAppearances false") // Signature flags (Table 225) // @@ -100,14 +100,14 @@ func (context *SignContext) createCatalog() (string, error) { // Set SigFlags and Permissions based on Signature Type switch context.SignData.Signature.CertType { case CertificationSignature, ApprovalSignature, TimeStampSignature: - catalogBuilder.WriteString(" /SigFlags 3") + catalog_buffer.WriteString(" /SigFlags 3") case UsageRightsSignature: - catalogBuilder.WriteString(" /SigFlags 1") + catalog_buffer.WriteString(" /SigFlags 1") } // Finalize the AcroForm and Catalog object - catalogBuilder.WriteString(" >>") // Close AcroForm - catalogBuilder.WriteString(" >>\nendobj\n") // Close catalog object + catalog_buffer.WriteString(" >>\n") // Close AcroForm + catalog_buffer.WriteString(">>\n") // Close Catalog - return catalogBuilder.String(), nil + return catalog_buffer.Bytes(), nil } diff --git a/sign/pdfcatalog_test.go b/sign/pdfcatalog_test.go index 3729a2c..5e68f01 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: "11 0 obj\n<< /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 3 >> >>\nendobj\n", - UsageRightsSignature: "11 0 obj\n<< /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 1 >> >>\nendobj\n", - ApprovalSignature: "11 0 obj\n<< /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 3 >> >>\nendobj\n", + 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", }, }, { file: "../testfiles/testfile21.pdf", expectedCatalogs: map[CertType]string{ - CertificationSignature: "17 0 obj\n<< /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 3 >> >>\nendobj\n", - UsageRightsSignature: "17 0 obj\n<< /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 1 >> >>\nendobj\n", - ApprovalSignature: "17 0 obj\n<< /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 3 >> >>\nendobj\n", + 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", }, }, } @@ -54,7 +54,6 @@ func TestCreateCatalog(t *testing.T) { } context := SignContext{ - Filesize: size + 1, PDFReader: rdr, InputFile: inputFile, VisualSignData: VisualSignData{ @@ -80,8 +79,8 @@ func TestCreateCatalog(t *testing.T) { return } - if catalog != expectedCatalog { - st.Errorf("Catalog mismatch, expected %s, but got %s", expectedCatalog, catalog) + if string(catalog) != expectedCatalog { + st.Errorf("Catalog mismatch, expected\n%s\nbut got\n%s", expectedCatalog, catalog) } }) } diff --git a/sign/pdfsignature.go b/sign/pdfsignature.go index aac2a01..30a298c 100644 --- a/sign/pdfsignature.go +++ b/sign/pdfsignature.go @@ -21,23 +21,20 @@ import ( const signatureByteRangePlaceholder = "/ByteRange[0 ********** ********** **********]" -func (context *SignContext) createSignaturePlaceholder() (dssd string, byte_range_start_byte int64, signature_contents_start_byte int64) { +func (context *SignContext) createSignaturePlaceholder() []byte { // Using a buffer because it's way faster than concatenating. var signature_buffer bytes.Buffer - signature_buffer.WriteString(strconv.Itoa(int(context.SignData.ObjectId)) + " 0 obj\n") - signature_buffer.WriteString("<< /Type /Sig\n") + + signature_buffer.WriteString("<<\n") + signature_buffer.WriteString(" /Type /Sig\n") signature_buffer.WriteString(" /Filter /Adobe.PPKLite\n") signature_buffer.WriteString(" /SubFilter /adbe.pkcs7.detached\n") signature_buffer.WriteString(context.createPropBuild()) - byte_range_start_byte = int64(signature_buffer.Len()) + 1 - // Create a placeholder for the byte range string, we will replace it later. signature_buffer.WriteString(" " + signatureByteRangePlaceholder) - signature_contents_start_byte = int64(signature_buffer.Len()) + 11 - // Create a placeholder for the actual signature content, we will replace it later. signature_buffer.WriteString(" /Contents<") signature_buffer.Write(bytes.Repeat([]byte("0"), int(context.SignatureMaxLength))) @@ -168,40 +165,47 @@ func (context *SignContext) createSignaturePlaceholder() (dssd string, byte_rang signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.ContactInfo)) signature_buffer.WriteString("\n") } - signature_buffer.WriteString(" /M ") - signature_buffer.WriteString(pdfDateTime(context.SignData.Signature.Info.Date)) - signature_buffer.WriteString("\n") - signature_buffer.WriteString(" >>\n") - signature_buffer.WriteString("endobj\n") + // (Optional) The time of signing. Depending on the signature handler, this may + // be a normal unverified computer time or a time generated in a verifiable way + // from a secure time server. + // + // This value should be used only when the time of signing is not available in the + // signature. If SubFilter is ETSI.RFC3161, this entry should not be used and + // should be ignored by a PDF processor. + // + // 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 == "" { + signature_buffer.WriteString(" /M ") + signature_buffer.WriteString(pdfDateTime(context.SignData.Signature.Info.Date)) + signature_buffer.WriteString("\n") + } - return signature_buffer.String(), byte_range_start_byte, signature_contents_start_byte + signature_buffer.WriteString(">>\n") + + return signature_buffer.Bytes() } -func (context *SignContext) createTimestampPlaceholder() (dssd string, byte_range_start_byte int64, signature_contents_start_byte int64) { +func (context *SignContext) createTimestampPlaceholder() []byte { var timestamp_buffer bytes.Buffer - timestamp_buffer.WriteString(strconv.Itoa(int(context.SignData.ObjectId)) + " 0 obj\n") - timestamp_buffer.WriteString("<< /Type /DocTimeStamp\n") + timestamp_buffer.WriteString("<<\n") + timestamp_buffer.WriteString(" /Type /DocTimeStamp\n") timestamp_buffer.WriteString(" /Filter /Adobe.PPKLite\n") timestamp_buffer.WriteString(" /SubFilter /ETSI.RFC3161\n") timestamp_buffer.WriteString(context.createPropBuild()) - byte_range_start_byte = int64(timestamp_buffer.Len()) + 1 - // Create a placeholder for the byte range string, we will replace it later. timestamp_buffer.WriteString(" " + signatureByteRangePlaceholder) - signature_contents_start_byte = int64(timestamp_buffer.Len()) + 11 - timestamp_buffer.WriteString(" /Contents<") timestamp_buffer.Write(bytes.Repeat([]byte("0"), int(context.SignatureMaxLength))) timestamp_buffer.WriteString(">\n") timestamp_buffer.WriteString(">>\n") - timestamp_buffer.WriteString("endobj\n") - return timestamp_buffer.String(), byte_range_start_byte, signature_contents_start_byte + return timestamp_buffer.Bytes() } func (context *SignContext) fetchRevocationData() error { @@ -446,16 +450,31 @@ func (context *SignContext) replaceSignature() error { } file_content := context.OutputBuffer.Buff.Bytes() - if _, err := context.OutputBuffer.Write(file_content[:(context.ByteRangeValues[0] + context.ByteRangeValues[1] + 1)]); err != nil { + // Write the file content up to the signature + if _, err := context.OutputBuffer.Write(file_content[context.ByteRangeValues[0]:context.ByteRangeValues[1]]); err != nil { + return err + } + + // Write new signature + if _, err := context.OutputBuffer.Write([]byte("<")); err != nil { return err } - // Write new ByteRange. if _, err := context.OutputBuffer.Write([]byte(dst)); err != nil { return err } - if _, err := context.OutputBuffer.Write(file_content[(context.ByteRangeValues[0]+context.ByteRangeValues[1]+1)+int64(len(dst)):]); err != nil { + // Write 0s to ensure the signature remains the same size + zeroPadding := bytes.Repeat([]byte("0"), int(context.SignatureMaxLength)-len(dst)) + if _, err := context.OutputBuffer.Write(zeroPadding); err != nil { + return err + } + + if _, err := context.OutputBuffer.Write([]byte(">")); err != nil { + return err + } + + if _, err := context.OutputBuffer.Write(file_content[context.ByteRangeValues[2] : context.ByteRangeValues[2]+context.ByteRangeValues[3]]); err != nil { return err } diff --git a/sign/pdfsignature_test.go b/sign/pdfsignature_test.go index 88a8d05..7aa31ed 100644 --- a/sign/pdfsignature_test.go +++ b/sign/pdfsignature_test.go @@ -1,100 +1,91 @@ package sign -// import ( -// "fmt" -// "os" -// "testing" -// "time" +import ( + "fmt" + "os" + "testing" + "time" -// "github.com/digitorus/pdf" -// ) + "github.com/digitorus/pdf" +) -// var signatureTests = []struct { -// file string -// expectedSignatures map[uint]string -// }{ -// { -// file: "../testfiles/testfile20.pdf", -// expectedSignatures: map[uint]string{ -// CertificationSignature: "13 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached /ByteRange[0 ********** ********** **********] /Contents<> /Reference [ << /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >> ] /Name (John Doe) /Location (Somewhere) /Reason (Test) /ContactInfo (None) /M (D:20170923143900+03'00') >>\nendobj\n", -// UsageRightsSignature: "13 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached /ByteRange[0 ********** ********** **********] /Contents<> /Reference [ << /Type /SigRef /TransformMethod /UR3 /TransformParams << /Type /TransformParams /V /2.2 >> >> ] /Name (John Doe) /Location (Somewhere) /Reason (Test) /ContactInfo (None) /M (D:20170923143900+03'00') >>\nendobj\n", -// ApprovalSignature: "13 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached /ByteRange[0 ********** ********** **********] /Contents<> /Reference [ << /Type /SigRef /TransformMethod /FieldMDP /TransformParams << /Type /TransformParams /Fields [<< /Type /SigFieldLock /Action /All >>] /V /1.2 >> >> ] /Name (John Doe) /Location (Somewhere) /Reason (Test) /ContactInfo (None) /M (D:20170923143900+03'00') >>\nendobj\n", -// }, -// }, -// } +var signatureTests = []struct { + file string + expectedSignatures map[CertType]string +}{ + { + file: "../testfiles/testfile20.pdf", + expectedSignatures: map[CertType]string{ + CertificationSignature: "<<\n /Type /Sig\n /Filter /Adobe.PPKLite\n /SubFilter /adbe.pkcs7.detached\n /Prop_Build <<\n /App << /Name /Digitorus#20PDFSign >>\n >>\n /ByteRange[0 ********** ********** **********] /Contents<>\n /Reference [\n << /Type /SigRef\n /TransformMethod /DocMDP\n /TransformParams <<\n /Type /TransformParams\n /P 2 /V /1.2\n >>\n >> ] /Name (John Doe)\n /Location (Somewhere)\n /Reason (Test)\n /ContactInfo (None)\n /M (D:20170923143900+03'00')\n>>\n", + UsageRightsSignature: "<<\n /Type /Sig\n /Filter /Adobe.PPKLite\n /SubFilter /adbe.pkcs7.detached\n /Prop_Build <<\n /App << /Name /Digitorus#20PDFSign >>\n >>\n /ByteRange[0 ********** ********** **********] /Contents<>\n /Reference [\n << /Type /SigRef\n /TransformMethod /UR3\n /TransformParams <<\n /Type /TransformParams\n /V /2.2\n >>\n >> ] /Name (John Doe)\n /Location (Somewhere)\n /Reason (Test)\n /ContactInfo (None)\n /M (D:20170923143900+03'00')\n>>\n", + ApprovalSignature: "<<\n /Type /Sig\n /Filter /Adobe.PPKLite\n /SubFilter /adbe.pkcs7.detached\n /Prop_Build <<\n /App << /Name /Digitorus#20PDFSign >>\n >>\n /ByteRange[0 ********** ********** **********] /Contents<>\n /TransformMethod /FieldMDP\n /TransformParams <<\n /Type /TransformParams\n /Action /All\n /V /1.2\n >>\n /Name (John Doe)\n /Location (Somewhere)\n /Reason (Test)\n /ContactInfo (None)\n /M (D:20170923143900+03'00')\n>>\n", + }, + }, +} -// func TestCreateSignaturePlaceholder(t *testing.T) { -// for _, testFile := range signatureTests { -// for certType, expectedSignature := range testFile.expectedSignatures { -// t.Run(fmt.Sprintf("%s_certType-%d", testFile.file, certType), func(st *testing.T) { -// inputFile, err := os.Open(testFile.file) -// if err != nil { -// st.Errorf("Failed to load test PDF") -// return -// } +func TestCreateSignaturePlaceholder(t *testing.T) { + for _, testFile := range signatureTests { + for certType, expectedSignature := range testFile.expectedSignatures { + t.Run(fmt.Sprintf("%s_certType-%d", testFile.file, certType), func(st *testing.T) { + inputFile, err := os.Open(testFile.file) + if err != nil { + st.Errorf("Failed to load test PDF") + return + } -// finfo, err := inputFile.Stat() -// if err != nil { -// st.Errorf("Failed to load test PDF") -// return -// } -// size := finfo.Size() + finfo, err := inputFile.Stat() + if err != nil { + st.Errorf("Failed to load test PDF") + return + } + size := finfo.Size() -// rdr, err := pdf.NewReader(inputFile, size) -// if err != nil { -// st.Errorf("Failed to load test PDF") -// return -// } + rdr, err := pdf.NewReader(inputFile, size) + if err != nil { + st.Errorf("Failed to load test PDF") + return + } -// timezone, _ := time.LoadLocation("Europe/Tallinn") -// now := time.Date(2017, 9, 23, 14, 39, 0, 0, timezone) + timezone, _ := time.LoadLocation("Europe/Tallinn") + now := time.Date(2017, 9, 23, 14, 39, 0, 0, timezone) -// sign_data := SignData{ -// Signature: SignDataSignature{ -// Info: SignDataSignatureInfo{ -// Name: "John Doe", -// Location: "Somewhere", -// Reason: "Test", -// ContactInfo: "None", -// Date: now, -// }, -// CertType: certType, -// DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms, -// }, -// } + sign_data := SignData{ + Signature: SignDataSignature{ + Info: SignDataSignatureInfo{ + Name: "John Doe", + Location: "Somewhere", + Reason: "Test", + ContactInfo: "None", + Date: now, + }, + CertType: certType, + DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms, + }, + } -// sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3 + sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3 -// context := SignContext{ -// Filesize: size + 1, -// 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, -// } + 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, + } -// signature, byte_range_start_byte, signature_contents_start_byte := context.createSignaturePlaceholder() + signature := context.createSignaturePlaceholder() -// if signature != expectedSignature { -// st.Errorf("Signature mismatch, expected %s, but got %s", expectedSignature, signature) -// } - -// if byte_range_start_byte != 78 { -// st.Errorf("Byte range start mismatch, expected %d, but got %d", 78, byte_range_start_byte) -// } - -// if signature_contents_start_byte != 135 { -// st.Errorf("Signature contents start byte mismatch, expected %d, but got %d", 135, signature_contents_start_byte) -// } -// }) -// } -// } -// } + if string(signature) != expectedSignature { + st.Errorf("Signature mismatch, expected:\n%q\nbut got:\n%q", expectedSignature, signature) + } + }) + } + } +} diff --git a/sign/pdftrailer.go b/sign/pdftrailer.go index ab677a1..e88c502 100644 --- a/sign/pdftrailer.go +++ b/sign/pdftrailer.go @@ -22,7 +22,7 @@ func (context *SignContext) writeTrailer() error { new_root := "Root " + strconv.FormatInt(int64(context.CatalogData.ObjectId), 10) + " 0 R" size_string := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) - new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+3, 10) + new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries)+1), 10) prev_string := "Prev " + context.PDFReader.Trailer().Key("Prev").String() new_prev := "Prev " + strconv.FormatInt(context.PDFReader.XrefInformation.StartPos, 10) diff --git a/sign/pdfvisualsignature.go b/sign/pdfvisualsignature.go index 1537881..2621b79 100644 --- a/sign/pdfvisualsignature.go +++ b/sign/pdfvisualsignature.go @@ -1,6 +1,7 @@ package sign import ( + "bytes" "fmt" "strconv" @@ -26,20 +27,20 @@ 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) (visual_signature string, err error) { - // Initialize the visual signature object with its ID. - visual_signature = strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 obj\n" +func (context *SignContext) createVisualSignature(visible bool, pageNumber int, rect [4]float64) ([]byte, error) { + var visual_signature bytes.Buffer + // Define the object as an annotation. - visual_signature += "<< /Type /Annot" + visual_signature.WriteString("<< /Type /Annot") // Specify the annotation subtype as a widget. - visual_signature += " /Subtype /Widget" + visual_signature.WriteString(" /Subtype /Widget") if visible { // Set the position and size of the signature field if visible. - visual_signature += 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]", rect[0], rect[1], rect[2], rect[3])) } else { // Set the rectangle to zero if the signature is invisible. - visual_signature += " /Rect [0 0 0 0]" + visual_signature.WriteString(" /Rect [0 0 0 0]") } // Retrieve the root object from the PDF trailer. @@ -64,7 +65,7 @@ func (context *SignContext) createVisualSignature(visible bool, pageNumber int, // Find the page object by its number. page, err := findPageByNumber(root.Key("Pages"), pageNumber) if err != nil { - return "", err + return nil, err } // Get the pointer to the page object. @@ -74,16 +75,16 @@ func (context *SignContext) createVisualSignature(visible bool, pageNumber int, context.VisualSignData.PageId = page_ptr.GetID() // Add the page reference to the visual signature. - visual_signature += " /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") } // Define the annotation flags for the signature field (132) // annotationFlags := AnnotationFlagPrint | AnnotationFlagNoZoom | AnnotationFlagNoRotate | AnnotationFlagReadOnly | AnnotationFlagLockedContents - visual_signature += fmt.Sprintf(" /F %d", 132) + visual_signature.WriteString(fmt.Sprintf(" /F %d", 132)) // Define the field type as a signature. - visual_signature += " /FT /Sig" + visual_signature.WriteString(" /FT /Sig") // Set a unique title for the signature field. - visual_signature += " /T " + pdfString("Signature "+strconv.Itoa(len(context.SignData.ExistingSignatures)+1)) + 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 @@ -92,16 +93,15 @@ func (context *SignContext) createVisualSignature(visible bool, pageNumber int, // (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 += " /Ff 0" + visual_signature.WriteString(" /Ff 0") // Reference the signature dictionary. - visual_signature += " /V " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R" + visual_signature.WriteString(" /V " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R") // Close the dictionary and end the object. - visual_signature += " >>" - visual_signature += "\nendobj\n" + visual_signature.WriteString(" >>\n") - return visual_signature, nil + return visual_signature.Bytes(), nil } // Helper function to find a page by its number. diff --git a/sign/pdfvisualsignature_test.go b/sign/pdfvisualsignature_test.go index 5360a8c..6b2037c 100644 --- a/sign/pdfvisualsignature_test.go +++ b/sign/pdfvisualsignature_test.go @@ -48,7 +48,6 @@ func TestVisualSignature(t *testing.T) { sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3 context := SignContext{ - Filesize: size + 1, PDFReader: rdr, InputFile: input_file, VisualSignData: VisualSignData{ @@ -63,7 +62,7 @@ func TestVisualSignature(t *testing.T) { SignData: sign_data, } - expected_visual_signature := "10 0 obj\n<< /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 >>\nendobj\n" + 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" visual_signature, err := context.createVisualSignature(false, 1, [4]float64{0, 0, 0, 0}) if err != nil { @@ -71,7 +70,7 @@ func TestVisualSignature(t *testing.T) { return } - if visual_signature != expected_visual_signature { - t.Errorf("Visual signature mismatch, expected %s, but got %s", expected_visual_signature, visual_signature) + if string(visual_signature) != expected_visual_signature { + t.Errorf("Visual signature mismatch, expected\n%q\nbut got\n%q", expected_visual_signature, visual_signature) } } diff --git a/sign/pdfxref.go b/sign/pdfxref.go index 3f17d2b..ba61225 100644 --- a/sign/pdfxref.go +++ b/sign/pdfxref.go @@ -12,15 +12,60 @@ import ( "strings" ) +type xrefEntry struct { + ID uint32 + Offset int64 +} + const ( xrefStreamColumns = 5 xrefStreamPredictor = 12 pngSubPredictor = 11 pngUpPredictor = 12 + objectFooter = "\nendobj\n" ) +func (context *SignContext) addObject(object []byte) (uint32, error) { + if context.lastXrefID == 0 { + lastXrefID, err := context.getLastObjectIDFromXref() + if err != nil { + return 0, fmt.Errorf("failed to get last object ID: %w", err) + } + context.lastXrefID = lastXrefID + } + + objectID := context.lastXrefID + uint32(len(context.newXrefEntries)) + 1 + context.newXrefEntries = append(context.newXrefEntries, xrefEntry{ + ID: objectID, + Offset: int64(context.OutputBuffer.Buff.Len()) + 1, + }) + + // 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) + } + + // 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) + } + + // 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 objectID, nil +} + // writeXref writes the cross-reference table or stream based on the PDF type. func (context *SignContext) writeXref() error { + if _, err := context.OutputBuffer.Write([]byte("\n")); err != nil { + return fmt.Errorf("failed to write newline before xref: %w", err) + } + context.NewXrefStart = int64(context.OutputBuffer.Buff.Len()) + switch context.PDFReader.XrefInformation.Type { case "table": return context.writeIncrXrefTable() @@ -31,109 +76,56 @@ func (context *SignContext) writeXref() error { } } -// writeXrefTable writes the cross-reference table to the output buffer. -// -//nolint:unused -func (context *SignContext) writeXrefTable() error { +func (context *SignContext) getLastObjectIDFromXref() (uint32, error) { // Seek to the start of the xref table if _, err := context.InputFile.Seek(context.PDFReader.XrefInformation.StartPos, io.SeekStart); err != nil { - return fmt.Errorf("failed to seek to xref table: %w", err) + return 0, fmt.Errorf("failed to seek to xref table: %w", err) } // Read the existing xref table xrefContent := make([]byte, context.PDFReader.XrefInformation.Length) if _, err := context.InputFile.Read(xrefContent); err != nil { - return fmt.Errorf("failed to read xref table: %w", err) + return 0, fmt.Errorf("failed to read xref table: %w", err) } // Parse the xref header xrefLines := strings.Split(string(xrefContent), "\n") xrefHeader := strings.Fields(xrefLines[1]) if len(xrefHeader) != 2 { - return fmt.Errorf("invalid xref header format") + return 0, fmt.Errorf("invalid xref header format") } - firstObjectID, err := strconv.Atoi(xrefHeader[0]) + firstObjectID, err := strconv.ParseUint(xrefHeader[0], 10, 32) if err != nil { - return fmt.Errorf("invalid first object ID: %w", err) + return 0, fmt.Errorf("invalid first object ID: %w", err) } - itemCount, err := strconv.Atoi(xrefHeader[1]) + itemCount, err := strconv.ParseUint(xrefHeader[1], 10, 32) if err != nil { - return fmt.Errorf("invalid item count: %w", err) + return 0, fmt.Errorf("invalid item count: %w", err) } - // Calculate new entries - newEntries := []struct { - startPosition int64 - name string - }{ - {context.Filesize, "visual signature"}, - {context.Filesize + context.VisualSignData.Length, "catalog"}, - {context.Filesize + context.VisualSignData.Length + context.CatalogData.Length, "signature"}, - } - - // Write new xref table - newXrefHeader := fmt.Sprintf("xref\n%d %d\n", firstObjectID, itemCount+len(newEntries)) - if _, err := context.OutputBuffer.Write([]byte(newXrefHeader)); err != nil { - return fmt.Errorf("failed to write new xref header: %w", err) - } - - // Write existing entries - for i, line := range xrefLines[2:] { - if i >= itemCount { - break - } - if _, err := context.OutputBuffer.Write([]byte(line + "\n")); err != nil { - return fmt.Errorf("failed to write existing xref entry: %w", err) - } - } - - // Write new entries - for _, entry := range newEntries { - xrefLine := fmt.Sprintf("%010d 00000 n\r\n", entry.startPosition) - if _, err := context.OutputBuffer.Write([]byte(xrefLine)); err != nil { - return fmt.Errorf("failed to write new xref entry for %s: %w", entry.name, err) - } - } - - return nil + return uint32(firstObjectID + itemCount), nil } // writeIncrXrefTable writes the incremental cross-reference table to the output buffer. func (context *SignContext) writeIncrXrefTable() error { - // Seek to the start of the xref table - if _, err := context.InputFile.Seek(context.PDFReader.XrefInformation.StartPos, io.SeekStart); err != nil { - return fmt.Errorf("failed to seek to xref table: %w", err) - } - - // Calculate new entries - newEntries := []struct { - objectID uint32 - startPosition int64 - name string - }{ - {context.VisualSignData.ObjectId, context.Filesize, "visual signature"}, - {context.CatalogData.ObjectId, context.Filesize + context.VisualSignData.Length, "catalog"}, - {context.SignData.ObjectId, context.Filesize + context.VisualSignData.Length + context.CatalogData.Length, "signature"}, - } - // Write xref header if _, err := context.OutputBuffer.Write([]byte("xref\n")); err != nil { return fmt.Errorf("failed to write incremental xref header: %w", err) } // Write xref subsection header - startXrefObj := fmt.Sprintf("%d %d\n", newEntries[0].objectID, len(newEntries)) + startXrefObj := fmt.Sprintf("%d %d\n", context.lastXrefID+1, len(context.newXrefEntries)) if _, err := context.OutputBuffer.Write([]byte(startXrefObj)); err != nil { return fmt.Errorf("failed to write starting xref object: %w", err) } // Write new entries - for _, entry := range newEntries { - xrefLine := fmt.Sprintf("%010d 00000 n \r\n", entry.startPosition) + for _, entry := range context.newXrefEntries { + 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 incremental xref entry for %s: %w", entry.name, err) + return fmt.Errorf("failed to write incremental xref entry: %w", err) } } @@ -168,17 +160,8 @@ func (context *SignContext) writeXrefStream() error { // writeXrefStreamEntries writes the individual entries for the xref stream. func writeXrefStreamEntries(buffer *bytes.Buffer, context *SignContext) error { - entries := []struct { - offset int64 - }{ - {context.Filesize}, - {context.Filesize + context.VisualSignData.Length}, - {context.Filesize + context.VisualSignData.Length + context.CatalogData.Length}, - {context.NewXrefStart}, - } - - for _, entry := range entries { - writeXrefStreamLine(buffer, 1, int(entry.offset), 0) + for _, entry := range context.newXrefEntries { + writeXrefStreamLine(buffer, 1, int(entry.Offset), 0) } return nil @@ -219,7 +202,7 @@ func writeXrefStreamHeader(context *SignContext, streamLength int) error { xrefStreamColumns, xrefStreamPredictor, context.PDFReader.XrefInformation.StartPos, - context.PDFReader.XrefInformation.ItemCount+4, + context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries))+1, context.PDFReader.XrefInformation.ItemCount, newRoot, id0, diff --git a/sign/sign.go b/sign/sign.go index c061777..ed17796 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -17,7 +17,6 @@ import ( type CatalogData struct { ObjectId uint32 - Length int64 RootString string } @@ -45,12 +44,10 @@ type SignData struct { type VisualSignData struct { PageId uint32 ObjectId uint32 - Length int64 } type InfoData struct { ObjectId uint32 - Length int64 } //go:generate stringer -type=CertType @@ -87,21 +84,21 @@ type SignDataSignatureInfo struct { } type SignContext struct { - Filesize int64 - InputFile io.ReadSeeker - OutputFile io.Writer - OutputBuffer *filebuffer.Buffer - SignData SignData - CatalogData CatalogData - VisualSignData VisualSignData - InfoData InfoData - PDFReader *pdf.Reader - NewXrefStart int64 - ByteRangeStartByte int64 - SignatureContentsStartByte int64 - ByteRangeValues []int64 - SignatureMaxLength uint32 - SignatureMaxLengthBase uint32 + InputFile io.ReadSeeker + OutputFile io.Writer + OutputBuffer *filebuffer.Buffer + SignData SignData + CatalogData CatalogData + VisualSignData VisualSignData + InfoData InfoData + PDFReader *pdf.Reader + NewXrefStart int64 + ByteRangeValues []int64 + SignatureMaxLength uint32 + SignatureMaxLengthBase uint32 + + lastXrefID uint32 + newXrefEntries []xrefEntry } func SignFile(input string, output string, sign_data SignData) error { @@ -134,9 +131,7 @@ 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 - // We do size+1 because we insert a newline. context := SignContext{ - Filesize: size + 1, PDFReader: rdr, InputFile: input, OutputFile: output, @@ -265,6 +260,22 @@ func (context *SignContext) SignPDF() error { context.SignatureMaxLength += uint32(hex.EncodedLen(9000)) } + // Create the signature object + var signature_object []byte + + switch context.SignData.Signature.CertType { + case TimeStampSignature: + signature_object = context.createTimestampPlaceholder() + default: + signature_object = context.createSignaturePlaceholder() + } + + // Write the new 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 // Example usage: passing page number and default rect values @@ -273,69 +284,45 @@ func (context *SignContext) SignPDF() error { return fmt.Errorf("failed to create visual signature: %w", err) } - context.VisualSignData.Length = int64(len(visual_signature)) - // Write the new visual signature object. - if _, err := context.OutputBuffer.Write([]byte(visual_signature)); err != nil { - return err + context.VisualSignData.ObjectId, err = context.addObject(visual_signature) + if err != nil { + return fmt.Errorf("failed to add visual signature object: %w", err) } + // Create a new catalog object catalog, err := context.createCatalog() if err != nil { return fmt.Errorf("failed to create catalog: %w", err) } - context.CatalogData.Length = int64(len(catalog)) - - // Write the new catalog object. - if _, err := context.OutputBuffer.Write([]byte(catalog)); err != nil { - return err + // Write the new catalog object + context.CatalogData.ObjectId, err = context.addObject(catalog) + if err != nil { + return fmt.Errorf("failed to add catalog object: %w", err) } - // Create the signature object - var signature_object string - var byte_range_start_byte, signature_contents_start_byte int64 - - switch context.SignData.Signature.CertType { - case TimeStampSignature: - signature_object, byte_range_start_byte, signature_contents_start_byte = context.createTimestampPlaceholder() - default: - signature_object, byte_range_start_byte, signature_contents_start_byte = context.createSignaturePlaceholder() - } - - appended_bytes := context.Filesize + int64(len(catalog)) + int64(len(visual_signature)) - - // Positions are relative to old start position of xref table. - byte_range_start_byte += appended_bytes - signature_contents_start_byte += appended_bytes - - context.ByteRangeStartByte = byte_range_start_byte - context.SignatureContentsStartByte = signature_contents_start_byte - - // Write the new signature object. - if _, err := context.OutputBuffer.Write([]byte(signature_object)); err != nil { - return fmt.Errorf("failed to create the new signature object: %w", err) - } - - // Calculate the new start position of the xref table. - context.NewXrefStart = appended_bytes + int64(len(signature_object)) - + // Write xref table if err := context.writeXref(); err != nil { return fmt.Errorf("failed to write xref: %w", err) } + // Write trailer if err := context.writeTrailer(); err != nil { return fmt.Errorf("failed to write trailer: %w", err) } + // Update byte range if err := context.updateByteRange(); err != nil { return fmt.Errorf("failed to update byte range: %w", err) } + // Replace signature if err := context.replaceSignature(); err != nil { - return err + return fmt.Errorf("failed to replace signature: %w", err) } + // Write final output if _, err := context.OutputBuffer.Seek(0, 0); err != nil { return err } diff --git a/sign/sign_test.go b/sign/sign_test.go index 6951c61..5055dd1 100644 --- a/sign/sign_test.go +++ b/sign/sign_test.go @@ -217,6 +217,8 @@ func TestSignPDFFileUTF8(t *testing.T) { if err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil { t.Error(err) } + } else if len(info.Signers) == 0 { + t.Fatalf("no signers found in %s", tmpfile.Name()) } else { if info.Signers[0].Name != signerName { t.Fatalf("expected %q, got %q", signerName, info.Signers[0].Name)