Refactor object handling

This commit is contained in:
Paul van Brouwershaven
2024-12-11 16:24:26 +01:00
parent 21dd78d75e
commit e5cdb61bea
11 changed files with 311 additions and 327 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
})
}

View File

@@ -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
}

View File

@@ -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)
}
})
}
}
}

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)