From 5bda988b184c71fbaa255127430b0f4e5008da6e Mon Sep 17 00:00:00 2001 From: Ivan Wallis Date: Tue, 31 Dec 2024 09:30:25 -0800 Subject: [PATCH] initial pdf visual signature support --- EXPERIMENTAL.md | 13 +- cmd/vsign/cli/options/pdf.go | 2 + go.mod | 2 +- go.sum | 2 + pkg/plugin/signers/pdf/signer.go | 30 +- pkg/provider/pdfsig/appearance.go | 62 +++ pkg/provider/pdfsig/certtype_string.go | 25 ++ pkg/provider/pdfsig/helpers.go | 3 +- pkg/provider/pdfsig/pdfbyterange.go | 60 +-- pkg/provider/pdfsig/pdfcatalog.go | 129 ++++-- pkg/provider/pdfsig/pdfsignature.go | 454 ++++++++++++++++------ pkg/provider/pdfsig/pdftrailer.go | 27 +- pkg/provider/pdfsig/pdfvisualsignature.go | 160 +++++++- pkg/provider/pdfsig/pdfxref.go | 275 +++++++++---- pkg/provider/pdfsig/sign.go | 254 ++++++------ 15 files changed, 1101 insertions(+), 397 deletions(-) create mode 100644 pkg/provider/pdfsig/appearance.go create mode 100644 pkg/provider/pdfsig/certtype_string.go diff --git a/EXPERIMENTAL.md b/EXPERIMENTAL.md index 5d3b825..fa030b3 100644 --- a/EXPERIMENTAL.md +++ b/EXPERIMENTAL.md @@ -69,4 +69,15 @@ vsign verify --config test/config.ini --payload test/hello.jar --signature test/ #### Cosign Image Signing ``` vsign sign --config test/config.ini --image myorg/myapp:v1 --mechanism 64 - ``` \ No newline at end of file + ``` + +#### PDF Signing with Visual Signatures + +Initial (experimental) support for PDF visual signatures based on [digitorus/pdfsign](https://github.com/digitorus/pdfsign) commit [b9112bb](https://github.com/digitorus/pdfsign/commit/b9112bb85ba5e2439bfacae2ce694e7f1cb66db1). Currently only available on [main](https://github.com/Venafi/vsign) branch + +``` +git clone https://github.com/Venafi/vsign +cd vsign +make vsign +./vsign sign --config test/config.ini --payload test/dummy.pdf --output-signature test/dummy-signed.pdf --digest sha256 --mechanism 1 --name "John Doe" --location "Pleasantville" --reason "Contract" --contact "john@doe.com" --visual +``` \ No newline at end of file diff --git a/cmd/vsign/cli/options/pdf.go b/cmd/vsign/cli/options/pdf.go index 65c45d6..c4a9a3f 100644 --- a/cmd/vsign/cli/options/pdf.go +++ b/cmd/vsign/cli/options/pdf.go @@ -26,6 +26,7 @@ type PDFOptions struct { Reason string Contact string TSA string + Visual bool } var _ Interface = (*PDFOptions)(nil) @@ -38,5 +39,6 @@ func (o *PDFOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.Reason, "reason", "Contract", "Reason for signing") cmd.Flags().StringVar(&o.Contact, "contact", "acme@example.com", "Contact information for the signatory") cmd.Flags().StringVar(&o.TSA, "tsa", "http://timestamp.digicert.com", "URL for Time-Stamp Authority (default: http://timestamp.digicert.com)") + cmd.Flags().BoolVar(&o.Visual, "visual", false, "add visual signature to pdf") } diff --git a/go.mod b/go.mod index 99e93a2..c31bc57 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.0 require ( github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/digitorus/pdf v0.1.2 - github.com/digitorus/pdfsign v0.0.0-20230417185736-110438cfb75c + github.com/digitorus/pdfsign v0.0.0-20241216140527-b9112bb85ba5 github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 github.com/mattetti/filebuffer v1.0.1 diff --git a/go.sum b/go.sum index 667c1c0..ffe6d3f 100644 --- a/go.sum +++ b/go.sum @@ -216,6 +216,8 @@ github.com/digitorus/pdf v0.1.2 h1:RjYEJNbiV6Kcn8QzRi6pwHuOaSieUUrg4EZo4b7KuIQ= github.com/digitorus/pdf v0.1.2/go.mod h1:05fDDJhPswBRM7GTfqCxNiDyeNcN0f+IobfOAl5pdXw= github.com/digitorus/pdfsign v0.0.0-20230417185736-110438cfb75c h1:q4b/wavoB57pJUeDczC0IB9HBXy/+QCT4n85aG5LfWI= github.com/digitorus/pdfsign v0.0.0-20230417185736-110438cfb75c/go.mod h1:7gT0P/9aEfe820SEQKH1auoMa8iBHhYnzKPjRz+8Fco= +github.com/digitorus/pdfsign v0.0.0-20241216140527-b9112bb85ba5 h1:k8ZdAoisy/UteAvWWm2ikauWzIV3HPUoOauJBqiQLvM= +github.com/digitorus/pdfsign v0.0.0-20241216140527-b9112bb85ba5/go.mod h1:sEZvpFG5EFDw6O7cXz6RbPPVr0+X5EQP42O8/TN+8a0= github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= diff --git a/pkg/plugin/signers/pdf/signer.go b/pkg/plugin/signers/pdf/signer.go index fe4a735..cf6090f 100644 --- a/pkg/plugin/signers/pdf/signer.go +++ b/pkg/plugin/signers/pdf/signer.go @@ -25,6 +25,7 @@ import ( "os" "time" + "github.com/common-nighthawk/go-figure" "github.com/venafi/vsign/cmd/vsign/cli/options" c "github.com/venafi/vsign/pkg/crypto" "github.com/venafi/vsign/pkg/plugin/signers" @@ -49,6 +50,7 @@ func init() { PDFSigner.Flags().String("reason", "Contract", "Reason for signing") PDFSigner.Flags().String("contact", "acme@example.com", "Contact information for the signatory") PDFSigner.Flags().String("tsa", "http://timestamp.digicert.com", "URL for Time-Stamp Authority (default: http://timestamp.digicert.com)") + PDFSigner.Flags().Bool("visual", false, "add visual signature to pdf") signers.Register(PDFSigner) } @@ -72,6 +74,31 @@ func sign(r io.Reader, certs []*x509.Certificate, opts signers.SignOpts) ([]byte _, hasher, _ := c.GetHasher(opts.Digest) + var ctype pdfsig.CertType + var app pdfsig.Appearance + + if opts.Flags.GetBool("visual") { + experimental := figure.NewFigure("experimental: pdf signing with visual signatures", "", true) + experimental.Print() + ctype = pdfsig.ApprovalSignature + app = pdfsig.Appearance{ + Visible: true, + LowerLeftX: 350, + LowerLeftY: 75, + UpperRightX: 600, + UpperRightY: 100, + } + } else { + ctype = pdfsig.CertificationSignature + app = pdfsig.Appearance{ + Visible: false, + LowerLeftX: 350, + LowerLeftY: 75, + UpperRightX: 600, + UpperRightY: 100, + } + } + signedPayload, err := pdfsig.SignFile(r, pdfsig.SignData{ Signature: pdfsig.SignDataSignature{ Info: pdfsig.SignDataSignatureInfo{ @@ -81,10 +108,11 @@ func sign(r io.Reader, certs []*x509.Certificate, opts signers.SignOpts) ([]byte ContactInfo: opts.Flags.GetString("contact"), Date: time.Now().Local(), }, - CertType: pdfsig.CertificationSignature, + CertType: ctype, DocMDPPerm: pdfsig.AllowFillingExistingFormFieldsAndSignaturesPerms, }, TPPOpts: opts, + Appearance: app, DigestAlgorithm: hasher, Certificate: cert.Leaf, CertificateChains: certificate_chains, diff --git a/pkg/provider/pdfsig/appearance.go b/pkg/provider/pdfsig/appearance.go new file mode 100644 index 0000000..14fac24 --- /dev/null +++ b/pkg/provider/pdfsig/appearance.go @@ -0,0 +1,62 @@ +package pdfsig + +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/pkg/provider/pdfsig/certtype_string.go b/pkg/provider/pdfsig/certtype_string.go new file mode 100644 index 0000000..3e9daae --- /dev/null +++ b/pkg/provider/pdfsig/certtype_string.go @@ -0,0 +1,25 @@ +package pdfsig + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[CertificationSignature-1] + _ = x[ApprovalSignature-2] + _ = x[UsageRightsSignature-3] + _ = x[TimeStampSignature-4] +} + +const _CertType_name = "CertificationSignatureApprovalSignatureUsageRightsSignatureTimeStampSignature" + +var _CertType_index = [...]uint8{0, 22, 39, 59, 77} + +func (i CertType) String() string { + i -= 1 + if i >= CertType(len(_CertType_index)-1) { + return "CertType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _CertType_name[_CertType_index[i]:_CertType_index[i+1]] +} diff --git a/pkg/provider/pdfsig/helpers.go b/pkg/provider/pdfsig/helpers.go index f5c1fe6..5aac873 100644 --- a/pkg/provider/pdfsig/helpers.go +++ b/pkg/provider/pdfsig/helpers.go @@ -21,7 +21,8 @@ func findFirstPage(parent pdf.Value) (pdf.Value, error) { if value_type == "/Pages" { for i := 0; i < parent.Key("Kids").Len(); i++ { - recurse_parent, recurse_err := findFirstPage(parent.Key("Kids").Index(i)) + kid := parent.Key("Kids").Index(i) + recurse_parent, recurse_err := findFirstPage(kid) if recurse_err == nil { return recurse_parent, recurse_err } diff --git a/pkg/provider/pdfsig/pdfbyterange.go b/pkg/provider/pdfsig/pdfbyterange.go index ed88d68..0f74259 100644 --- a/pkg/provider/pdfsig/pdfbyterange.go +++ b/pkg/provider/pdfsig/pdfbyterange.go @@ -1,6 +1,7 @@ package pdfsig 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) - - // 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] + // 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") + } - new_byte_range := fmt.Sprintf("/ByteRange[%d %d %d %d]", context.ByteRangeValues[0], context.ByteRangeValues[1], context.ByteRangeValues[2], context.ByteRangeValues[3]) + // Calculate ByteRangeValues + signatureContentsStart := int64(contentsIndex) - 1 + signatureContentsEnd := signatureContentsStart + int64(context.SignatureMaxLength) + 2 + context.ByteRangeValues = []int64{ + 0, + signatureContentsStart, + signatureContentsEnd, + int64(context.OutputBuffer.Buff.Len()) - signatureContentsEnd, + } - // Make sure our ByteRange string didn't shrink in length. - new_byte_range += strings.Repeat(" ", len(signatureByteRangePlaceholder)-len(new_byte_range)) + new_byte_range := fmt.Sprintf("/ByteRange [%d %d %d %d]", context.ByteRangeValues[0], context.ByteRangeValues[1], context.ByteRangeValues[2], context.ByteRangeValues[3]) - if _, err := context.OutputBuffer.Seek(0, 0); 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") } - file_content := context.OutputBuffer.Buff.Bytes() - if _, err := context.OutputBuffer.Write(file_content[:context.ByteRangeStartByte]); 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") } - // Write new ByteRange. - if _, err := context.OutputBuffer.Write([]byte(new_byte_range)); err != nil { - return err - } + // 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)) - if _, err := context.OutputBuffer.Write(file_content[context.ByteRangeStartByte+int64(len(new_byte_range)):]); err != nil { + // 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/pkg/provider/pdfsig/pdfcatalog.go b/pkg/provider/pdfsig/pdfcatalog.go index d4dc1fe..cc54bce 100644 --- a/pkg/provider/pdfsig/pdfcatalog.go +++ b/pkg/provider/pdfsig/pdfcatalog.go @@ -1,58 +1,125 @@ package pdfsig import ( + "bytes" "strconv" ) -func (context *SignContext) createCatalog() (catalog string, err error) { - catalog = strconv.Itoa(int(context.CatalogData.ObjectId)) + " 0 obj\n" - catalog += "<< /Type /Catalog" - catalog += " /Version /" + context.PDFReader.PDFVersion +func (context *SignContext) createCatalog() ([]byte, error) { + var catalog_buffer bytes.Buffer + // Start the catalog object + catalog_buffer.WriteString("<<\n") + 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 + // specified in the file’s header (see 7.5.2, "File header"). If the header + // specifies a later version, or if this entry is absent, the document + // shall conform to the version specified in the header. This entry + // enables a PDF processor to update the version using an incremental + // update; see 7.5.6, "Incremental updates". + // The value of this entry shall be a name object, not a number, and + // therefore shall be preceded by a SOLIDUS (2Fh) character (/) when + // written in the PDF file (for example, /1.4). + // + // If an incremental upgrade requires a version that is higher than specified by the document. + // if context.PDFReader.PDFVersion < "2.0" { + // catalog_buffer.WriteString(" /Version /2.0") + // } + + // Retrieve the root and check for necessary keys in one loop root := context.PDFReader.Trailer().Key("Root") - root_keys := root.Keys() - found_pages := false - for _, key := range root_keys { - if key == "Pages" { - found_pages = true + rootPtr := root.GetPtr() + context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R" + + foundPages, foundNames := false, false + for _, key := range root.Keys() { + switch key { + case "Pages": + foundPages = true + case "Names": + foundNames = true + } + if foundPages && foundNames { break } } - rootPtr := root.GetPtr() - context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R" - - if found_pages { + // Add Pages and Names references if they exist + if foundPages { pages := root.Key("Pages").GetPtr() - catalog += " /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\n") } - catalog += " /AcroForm <<" - catalog += " /Fields [" + strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 R]" + // Start the AcroForm dictionary with /NeedAppearances + catalog_buffer.WriteString(" /AcroForm <<\n") + catalog_buffer.WriteString(" /Fields [") - switch context.SignData.Signature.CertType { - case CertificationSignature, UsageRightsSignature: - catalog += " /NeedAppearances false" + // Add existing signatures to the AcroForm dictionary + for i, sig := range context.existingSignatures { + if i > 0 { + catalog_buffer.WriteString(" ") + } + catalog_buffer.WriteString(strconv.Itoa(int(sig.objectId)) + " 0 R") } - switch context.SignData.Signature.CertType { - case CertificationSignature: - catalog += " /SigFlags 3" - case UsageRightsSignature: - catalog += " /SigFlags 1" + // Add the visual signature field to the AcroForm dictionary + if len(context.existingSignatures) > 0 { + catalog_buffer.WriteString(" ") } + catalog_buffer.WriteString(strconv.Itoa(int(context.VisualSignData.objectId)) + " 0 R") + + catalog_buffer.WriteString("]\n") // close Fields array - catalog += " >>" + // (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) + // + // Bit position 1: SignaturesExist + // If set, the document contains at least one signature field. This + // flag allows an interactive PDF processor to enable user + // interface items (such as menu items or push-buttons) related to + // signature processing without having to scan the entire + // document for the presence of signature fields. + // + // Bit position 2: AppendOnly + // If set, the document contains signatures that may be invalidated + // if the PDF file is saved (written) in a way that alters its previous + // contents, as opposed to an incremental update. Merely updating + // the PDF file by appending new information to the end of the + // previous version is safe (see H.7, "Updating example"). + // Interactive PDF processors may use this flag to inform a user + // requesting a full save that signatures will be invalidated and + // require explicit confirmation before continuing with the + // operation. + // + // Set SigFlags and Permissions based on Signature Type switch context.SignData.Signature.CertType { - case CertificationSignature: - catalog += " /Perms << /DocMDP " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>" + case CertificationSignature, ApprovalSignature, TimeStampSignature: + catalog_buffer.WriteString(" /SigFlags 3\n") case UsageRightsSignature: - catalog += " /Perms << /UR3 " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R >>" + catalog_buffer.WriteString(" /SigFlags 1\n") } - catalog += " >>" - catalog += "\nendobj\n" + // Finalize the AcroForm and Catalog object + catalog_buffer.WriteString(" >>\n") // Close AcroForm + catalog_buffer.WriteString(">>\n") // Close Catalog - return catalog, nil + return catalog_buffer.Bytes(), nil } diff --git a/pkg/provider/pdfsig/pdfsignature.go b/pkg/provider/pdfsig/pdfsignature.go index 7b89ced..db8d2fe 100644 --- a/pkg/provider/pdfsig/pdfsignature.go +++ b/pkg/provider/pdfsig/pdfsignature.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "io" + "log" "math/big" "net/http" "strconv" @@ -31,75 +32,191 @@ type issuerAndSerial struct { 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") - signature_buffer.WriteString(" /Filter /Adobe.PPKLite") - signature_buffer.WriteString(" /SubFilter /adbe.pkcs7.detached") - byte_range_start_byte = int64(signature_buffer.Len()) + 1 + 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()) // 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 wil replace it later. + // 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))) - signature_buffer.WriteString(">") + signature_buffer.WriteString(">\n") switch context.SignData.Signature.CertType { case CertificationSignature, UsageRightsSignature: - signature_buffer.WriteString(" /Reference [") // start array of signature reference dictionaries - signature_buffer.WriteString(" << /Type /SigRef") + signature_buffer.WriteString(" /Reference [\n") // start array of signature reference dictionaries + signature_buffer.WriteString(" << /Type /SigRef\n") } switch context.SignData.Signature.CertType { + // Certification signature (also known as an author signature) case CertificationSignature: - signature_buffer.WriteString(" /TransformMethod /DocMDP") - signature_buffer.WriteString(" /TransformParams <<") - signature_buffer.WriteString(" /Type /TransformParams") - signature_buffer.WriteString(" /P " + strconv.Itoa(int(context.SignData.Signature.DocMDPPerm))) - signature_buffer.WriteString(" /V /1.2") + signature_buffer.WriteString(" /TransformMethod /DocMDP\n") + + // Entries in the DocMDP transform parameters dictionary (Table 257) + signature_buffer.WriteString(" /TransformParams <<\n") + + // Type [name]: (Optional) The type of PDF object that this dictionary describes; + // if present, shall be TransformParams for a transform parameters dictionary. + signature_buffer.WriteString(" /Type /TransformParams\n") + + // (Optional) The access permissions granted for this document. Changes to + // a PDF that are incremental updates which include only the data necessary + // to add DSS’s 12.8.4.3, "Document Security Store (DSS)" and/or document + // timestamps 12.8.5, "Document timestamp (DTS) dictionary" to the + // document shall not be considered as changes to the document as defined + // in the choices below. + // + // Valid values shall be: + // 1 No changes to the document shall be permitted; any change to the document + // shall invalidate the signature. + // 2 Permitted changes shall be filling in forms, instantiating page templates, + // and signing; other changes shall invalidate the signature. + // 3 Permitted changes shall be the same as for 2, as well as annotation creation, + // deletion, and modification; other changes shall invalidate the signature. + // + // (Default value: 2.) + signature_buffer.WriteString(" /P " + strconv.Itoa(int(context.SignData.Signature.DocMDPPerm))) + + // V [name]: (Optional) The DocMDP transform parameters dictionary version. The only valid value shall be 1.2. + // Default value: 1.2. (This value is a name object, not a number.) + signature_buffer.WriteString(" /V /1.2\n") + + // Usage rights signature (deprecated in PDF 2.0) case UsageRightsSignature: - signature_buffer.WriteString(" /TransformMethod /UR3") - signature_buffer.WriteString(" /TransformParams <<") - signature_buffer.WriteString(" /Type /TransformParams") - signature_buffer.WriteString(" /V /2.2") + signature_buffer.WriteString(" /TransformMethod /UR3\n") + + // Entries in the UR transform parameters dictionary (Table 258) + signature_buffer.WriteString(" /TransformParams <<\n") + signature_buffer.WriteString(" /Type /TransformParams\n") + signature_buffer.WriteString(" /V /2.2\n") + + // Approval signatures (also known as recipient signatures) + case ApprovalSignature: + // Used to detect modifications to a list of form fields specified in TransformParams; see + // 12.8.2.4, "FieldMDP" + signature_buffer.WriteString(" /TransformMethod /FieldMDP\n") + + // Entries in the FieldMDP transform parameters dictionary (Table 259) + signature_buffer.WriteString(" /TransformParams <<\n") + + // Type [name]: (Optional) The type of PDF object that this dictionary describes; + // if present, shall be TransformParams for a transform parameters dictionary. + signature_buffer.WriteString(" /Type /TransformParams\n") + + // Action [name]: (Required) A name that, along with the Fields array, describes + // which form fields do not permit changes after the signature is applied. + // Valid values shall be: + // All - All form fields + // Include - Only those form fields specified in Fields. + // Exclude - Only those form fields not specified in Fields. + signature_buffer.WriteString(" /Action /All\n") + + // V [name]: (Optional; required for PDF 1.5 and later) The transform parameters + // dictionary version. The value for PDF 1.5 and later shall be 1.2. + // Default value: 1.2. (This value is a name object, not a number.) + signature_buffer.WriteString(" /V /1.2\n") + } + + // (Required) A name identifying the algorithm that shall be used when computing the digest if not specified in the + // certificate. Valid values are MD5, SHA1 SHA256, SHA384, SHA512 and RIPEMD160 + switch context.SignData.DigestAlgorithm { + case crypto.MD5: + signature_buffer.WriteString(" /DigestMethod /MD5\n") + case crypto.SHA1: + signature_buffer.WriteString(" /DigestMethod /SHA1\n") + case crypto.SHA256: + signature_buffer.WriteString(" /DigestMethod /SHA256\n") + case crypto.SHA384: + signature_buffer.WriteString(" /DigestMethod /SHA384\n") + case crypto.SHA512: + signature_buffer.WriteString(" /DigestMethod /SHA512\n") + case crypto.RIPEMD160: + signature_buffer.WriteString(" /DigestMethod /RIPEMD160\n") } switch context.SignData.Signature.CertType { case CertificationSignature, UsageRightsSignature: - signature_buffer.WriteString(" >>") // close TransformParams - signature_buffer.WriteString(" >>") - signature_buffer.WriteString(" ]") // end of reference + signature_buffer.WriteString(" >>\n") // close TransformParams + signature_buffer.WriteString(" >>") // close SigRef + signature_buffer.WriteString(" ]") // end of reference + } + + switch context.SignData.Signature.CertType { + case ApprovalSignature: + signature_buffer.WriteString(" >>\n") } if context.SignData.Signature.Info.Name != "" { signature_buffer.WriteString(" /Name ") signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.Name)) + signature_buffer.WriteString("\n") } if context.SignData.Signature.Info.Location != "" { signature_buffer.WriteString(" /Location ") signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.Location)) + signature_buffer.WriteString("\n") } if context.SignData.Signature.Info.Reason != "" { signature_buffer.WriteString(" /Reason ") signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.Reason)) + signature_buffer.WriteString("\n") } if context.SignData.Signature.Info.ContactInfo != "" { signature_buffer.WriteString(" /ContactInfo ") signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.ContactInfo)) + signature_buffer.WriteString("\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 == "" && !context.SignData.Signature.Info.Date.IsZero() { + signature_buffer.WriteString(" /M ") + signature_buffer.WriteString(pdfDateTime(context.SignData.Signature.Info.Date)) + signature_buffer.WriteString("\n") } - signature_buffer.WriteString(" /M ") - signature_buffer.WriteString(pdfDateTime(context.SignData.Signature.Info.Date)) - signature_buffer.WriteString(" >>") - signature_buffer.WriteString("\nendobj\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() []byte { + var timestamp_buffer bytes.Buffer + + 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()) + + // Create a placeholder for the byte range string, we will replace it later. + timestamp_buffer.WriteString(" " + signatureByteRangePlaceholder) + + timestamp_buffer.WriteString(" /Contents<") + timestamp_buffer.Write(bytes.Repeat([]byte("0"), int(context.SignatureMaxLength))) + timestamp_buffer.WriteString(">\n") + timestamp_buffer.WriteString(">>\n") + + return timestamp_buffer.Bytes() } func (context *SignContext) fetchRevocationData() error { @@ -168,26 +285,113 @@ func (context *SignContext) createSigningCertificateAttribute() (*Attribute, err return &signingCertificate, nil } -// verifyPartialChain checks that a given cert is issued by the first parent in the list, -// then continue down the path. It doesn't require the last parent to be a root CA, -// or to be trusted in any truststore. It simply verifies that the chain provided, albeit -// partial, makes sense. -func verifyPartialChain(cert *x509.Certificate, parents []*x509.Certificate) error { - if len(parents) == 0 { - return fmt.Errorf("pkcs7: zero parents provided to verify the signature of certificate %q", cert.Subject.CommonName) +func (context *SignContext) createSignature() ([]byte, error) { + if _, err := context.OutputBuffer.Seek(0, 0); err != nil { + return nil, err } - err := cert.CheckSignatureFrom(parents[0]) + + // Sadly we can't efficiently sign a file, we need to read all the bytes we want to sign. + file_content := context.OutputBuffer.Buff.Bytes() + + // Collect the parts to sign. + sign_content := make([]byte, 0) + sign_content = append(sign_content, file_content[context.ByteRangeValues[0]:(context.ByteRangeValues[0]+context.ByteRangeValues[1])]...) + sign_content = append(sign_content, file_content[context.ByteRangeValues[2]:(context.ByteRangeValues[2]+context.ByteRangeValues[3])]...) + + // Return the timestamp if we are signing a timestamp. + if context.SignData.Signature.CertType == TimeStampSignature { + // ETSI EN 319 142-1 V1.2.1 + // + // Contents [Byte string ]: (Required) When the value of SubFilter is ETSI.RFC3161, + // the value of Contents shall be the hexadecimal string (as defined in clause + // 7.3.4.3 in ISO 32000-1 [1]) representing the value of TimeStampToken as + // specified in IETF RFC 3161 [6] updated by IETF RFC 5816 [8]. The value of the + // messageImprint field within the TimeStampToken shall be a hash of the bytes + // of the document indicated by the ByteRange. The ByteRange shall cover the + // entire document, including the Document Time-stamp dictionary but excluding + // the TimeStampToken itself (the entry with key Contents). + + timestamp_response, err := context.GetTSA(sign_content) + if err != nil { + return nil, fmt.Errorf("get timestamp: %w", err) + } + + ts, err := timestamp.ParseResponse(timestamp_response) + if err != nil { + return nil, fmt.Errorf("parse timestamp: %w", err) + } + + return ts.RawToken, nil + } + + // Initialize pkcs7 signer. + signed_data, err := NewSignedData(sign_content) if err != nil { - return fmt.Errorf("pkcs7: certificate signature from parent is invalid: %v", err) + return nil, fmt.Errorf("new signed data: %w", err) } - if len(parents) == 1 { - // there is no more parent to check, return - return nil + + signed_data.SetDigestAlgorithm(getOIDFromHashAlgorithm(context.SignData.DigestAlgorithm)) + signingCertificate, err := context.createSigningCertificateAttribute() + if err != nil { + return nil, fmt.Errorf("new signed data: %w", err) } - return verifyPartialChain(parents[0], parents[1:]) + + signer_config := SignerInfoConfig{ + ExtraSignedAttributes: []Attribute{ + { + Type: asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, + Value: context.SignData.RevocationData, + }, + *signingCertificate, + }, + } + + // Add the first certificate chain without our own certificate. + var certificate_chain []*x509.Certificate + if len(context.SignData.CertificateChains) > 0 && len(context.SignData.CertificateChains[0]) > 1 { + certificate_chain = context.SignData.CertificateChains[0][1:] + } + + // Add the signer and sign the data. + // Custom signer to leverage CodeSign Protect + if err := context.addSignerChain(signed_data, context.SignData.Certificate, certificate_chain, signer_config); err != nil { + return nil, fmt.Errorf("add signer chain: %w", err) + } + + // PDF needs a detached signature, meaning the content isn't included. + signed_data.Detach() + + if context.SignData.TSA.URL != "" { + signature_data := signed_data.GetSignedData() + + timestamp_response, err := context.GetTSA(signature_data.SignerInfos[0].EncryptedDigest) + if err != nil { + return nil, fmt.Errorf("get timestamp: %w", err) + } + + ts, err := timestamp.ParseResponse(timestamp_response) + if err != nil { + return nil, fmt.Errorf("parse timestamp: %w", err) + } + + _, err = Parse(ts.RawToken) + if err != nil { + return nil, fmt.Errorf("parse timestamp token: %w", err) + } + + timestamp_attribute := Attribute{ + Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}, + Value: asn1.RawValue{FullBytes: ts.RawToken}, + } + if err := signature_data.SignerInfos[0].SetUnauthenticatedAttributes([]Attribute{timestamp_attribute}); err != nil { + return nil, err + } + } + + return signed_data.Finish() } -func signAttributes(attrs []attribute, pKey crypto.PublicKey, sd SignData, digestAlg crypto.Hash) ([]byte, error) { +func signAttributes(attrs []attribute, pKey crypto.PublicKey, sd SignData) ([]byte, error) { attrBytes, err := marshalAttributes(attrs) if err != nil { return nil, err @@ -280,7 +484,7 @@ func (context *SignContext) addSignerChain(sd *SignedData, ee *x509.Certificate, return err } // create signature of signed attributes - signature, err := signAttributes(finalAttrs, ee.PublicKey, context.SignData, hash) + signature, err := signAttributes(finalAttrs, ee.PublicKey, context.SignData) if err != nil { return err } @@ -303,89 +507,10 @@ func (context *SignContext) addSignerChain(sd *SignedData, ee *x509.Certificate, return nil } -func (context *SignContext) createSignature() ([]byte, error) { - if _, err := context.OutputBuffer.Seek(0, 0); err != nil { - return nil, err - } - - // Sadly we can't efficiently sign a file, we need to read all the bytes we want to sign. - file_content := context.OutputBuffer.Buff.Bytes() - - // Collect the parts to sign. - sign_content := make([]byte, 0) - sign_content = append(sign_content, file_content[context.ByteRangeValues[0]:(context.ByteRangeValues[0]+context.ByteRangeValues[1])]...) - sign_content = append(sign_content, file_content[context.ByteRangeValues[2]:(context.ByteRangeValues[2]+context.ByteRangeValues[3])]...) - - // Initialize pkcs7 signer. - signed_data, err := NewSignedData(sign_content) - if err != nil { - return nil, fmt.Errorf("new signed data: %w", err) - } - - signed_data.SetDigestAlgorithm(getOIDFromHashAlgorithm(context.SignData.DigestAlgorithm)) - signingCertificate, err := context.createSigningCertificateAttribute() - if err != nil { - return nil, fmt.Errorf("new signed data: %w", err) - } - - signer_config := SignerInfoConfig{ - ExtraSignedAttributes: []Attribute{ - { - Type: asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, - Value: context.SignData.RevocationData, - }, - *signingCertificate, - }, - } - - // Add the first certificate chain without our own certificate. - var certificate_chain []*x509.Certificate - if len(context.SignData.CertificateChains) > 0 && len(context.SignData.CertificateChains[0]) > 1 { - certificate_chain = context.SignData.CertificateChains[0][1:] - } - - // Add the signer and sign the data. - // Custom signer to leverage CodeSign Protect - if err := context.addSignerChain(signed_data, context.SignData.Certificate, certificate_chain, signer_config); err != nil { - return nil, fmt.Errorf("add signer chain: %w", err) - } - - // PDF needs a detached signature, meaning the content isn't included. - signed_data.Detach() - - if context.SignData.TSA.URL != "" { - signature_data := signed_data.GetSignedData() - - timestamp_response, err := context.GetTSA(signature_data.SignerInfos[0].EncryptedDigest) - if err != nil { - return nil, fmt.Errorf("get timestamp: %w", err) - } - - ts, err := timestamp.ParseResponse(timestamp_response) - if err != nil { - return nil, fmt.Errorf("parse timestamp: %w", err) - } - - _, err = Parse(ts.RawToken) - if err != nil { - return nil, fmt.Errorf("parse timestamp token: %w", err) - } - - timestamp_attribute := Attribute{ - Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}, - Value: asn1.RawValue{FullBytes: ts.RawToken}, - } - if err := signature_data.SignerInfos[0].SetUnauthenticatedAttributes([]Attribute{timestamp_attribute}); err != nil { - return nil, err - } - } - - return signed_data.Finish() -} - func (context *SignContext) GetTSA(sign_content []byte) (timestamp_response []byte, err error) { sign_reader := bytes.NewReader(sign_content) ts_request, err := timestamp.CreateRequest(sign_reader, ×tamp.RequestOptions{ + Hash: context.SignData.DigestAlgorithm, Certificates: true, }) if err != nil { @@ -442,7 +567,7 @@ func (context *SignContext) replaceSignature() ([]byte, error) { hex.Encode(dst, signature) if uint32(len(dst)) > context.SignatureMaxLength { - // TODO: Should we log this retry? + log.Println("Signature too long, retrying with increased buffer size.") // set new base and try signing again context.SignatureMaxLengthBase += (uint32(len(dst)) - context.SignatureMaxLength) + 1 return context.SignPDF() @@ -453,18 +578,91 @@ func (context *SignContext) replaceSignature() ([]byte, 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 nil, err + } + + // Write new signature + if _, err := context.OutputBuffer.Write([]byte("<")); err != nil { return nil, err } - // Write new ByteRange. if _, err := context.OutputBuffer.Write([]byte(dst)); err != nil { return nil, 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 nil, err + } + + if _, err := context.OutputBuffer.Write([]byte(">")); err != nil { + return nil, err + } + + if _, err := context.OutputBuffer.Write(file_content[context.ByteRangeValues[2] : context.ByteRangeValues[2]+context.ByteRangeValues[3]]); err != nil { return nil, err } return file_content, nil } + +func verifyPartialChain(cert *x509.Certificate, parents []*x509.Certificate) error { + if len(parents) == 0 { + return fmt.Errorf("pkcs7: zero parents provided to verify the signature of certificate %q", cert.Subject.CommonName) + } + err := cert.CheckSignatureFrom(parents[0]) + if err != nil { + return fmt.Errorf("pkcs7: certificate signature from parent is invalid: %v", err) + } + if len(parents) == 1 { + // there is no more parent to check, return + return nil + } + return verifyPartialChain(parents[0], parents[1:]) +} + +func (context *SignContext) fetchExistingSignatures() ([]SignData, error) { + var signatures []SignData + + acroForm := context.PDFReader.Trailer().Key("Root").Key("AcroForm") + if acroForm.IsNull() { + return signatures, nil + } + + fields := acroForm.Key("Fields") + if fields.IsNull() { + return signatures, nil + } + + for i := 0; i < fields.Len(); i++ { + field := fields.Index(i) + if field.Key("FT").Name() == "Sig" { + ptr := field.GetPtr() + sig := SignData{ + objectId: uint32(ptr.GetID()), + } + signatures = append(signatures, sig) + } + } + + return signatures, nil +} + +func (context *SignContext) createPropBuild() string { + var buffer bytes.Buffer + + // Prop_Build [dictionary]: (Optional; PDF 1.5) A dictionary that may be used by a signature handler to + // record information that captures the state of the computer environment used + // for signing, such as the name of the handler used to create the signature, + // software build date, version, and operating system. + // The use of this dictionary is defined by Adobe PDF Signature Build Dictionary + // Specification, which provides implementation guidelines. + buffer.WriteString(" /Prop_Build <<\n") + buffer.WriteString(" /App << /Name /Digitorus#20PDFSign >>\n") + buffer.WriteString(" >>\n") + + return buffer.String() +} diff --git a/pkg/provider/pdfsig/pdftrailer.go b/pkg/provider/pdfsig/pdftrailer.go index cbb760a..a3b01af 100644 --- a/pkg/provider/pdfsig/pdftrailer.go +++ b/pkg/provider/pdfsig/pdftrailer.go @@ -6,7 +6,6 @@ import ( ) func (context *SignContext) writeTrailer() error { - if context.PDFReader.XrefInformation.Type == "table" { trailer_length := context.PDFReader.XrefInformation.IncludingTrailerEndPos - context.PDFReader.XrefInformation.EndPos @@ -23,18 +22,28 @@ 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+4, 10) - - info := context.PDFReader.Trailer().Key("Info") - infoPtr := info.GetPtr() + new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries)+1), 10) - info_string := "Info " + strconv.Itoa(int(infoPtr.GetID())) + " " + strconv.Itoa(int(infoPtr.GetGen())) + " R" - new_info := "Info " + strconv.FormatInt(int64(context.InfoData.ObjectId), 10) + " 0 R" + prev_string := "Prev " + context.PDFReader.Trailer().Key("Prev").String() + new_prev := "Prev " + strconv.FormatInt(context.PDFReader.XrefInformation.StartPos, 10) trailer_string := string(trailer_buf) trailer_string = strings.Replace(trailer_string, root_string, new_root, -1) trailer_string = strings.Replace(trailer_string, size_string, new_size, -1) - trailer_string = strings.Replace(trailer_string, info_string, new_info, -1) + if strings.Contains(trailer_string, prev_string) { + trailer_string = strings.Replace(trailer_string, prev_string, new_prev, -1) + } else { + trailer_string = strings.Replace(trailer_string, new_root, new_root+"\n /"+new_prev, -1) + } + + // Ensure the same amount of padding (two spaces) for each line, except when the line does not start with a whitespace already. + lines := strings.Split(trailer_string, "\n") + for i, line := range lines { + if strings.HasPrefix(line, " ") { + lines[i] = " " + strings.TrimSpace(line) + } + } + trailer_string = strings.Join(lines, "\n") // Write the new trailer. if _, err := context.OutputBuffer.Write([]byte(trailer_string)); err != nil { @@ -48,7 +57,7 @@ func (context *SignContext) writeTrailer() error { } // Write PDF ending. - if _, err := context.OutputBuffer.Write([]byte("%%EOF")); err != nil { + if _, err := context.OutputBuffer.Write([]byte("%%EOF\n")); err != nil { return err } diff --git a/pkg/provider/pdfsig/pdfvisualsignature.go b/pkg/provider/pdfsig/pdfvisualsignature.go index 197b76e..19dfc3b 100644 --- a/pkg/provider/pdfsig/pdfvisualsignature.go +++ b/pkg/provider/pdfsig/pdfvisualsignature.go @@ -1,47 +1,173 @@ package pdfsig import ( + "bytes" + "fmt" "strconv" + + "github.com/digitorus/pdf" +) + +// Define annotation flag constants. +const ( + AnnotationFlagInvisible = 1 << 0 + AnnotationFlagHidden = 1 << 1 + AnnotationFlagPrint = 1 << 2 + AnnotationFlagNoZoom = 1 << 3 + AnnotationFlagNoRotate = 1 << 4 + AnnotationFlagNoView = 1 << 5 + AnnotationFlagReadOnly = 1 << 6 + AnnotationFlagLocked = 1 << 7 + AnnotationFlagToggleNoView = 1 << 8 + AnnotationFlagLockedContents = 1 << 9 ) -func (context *SignContext) createVisualSignature() (visual_signature string, err error) { - visual_signature = strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 obj\n" - visual_signature += "<< /Type /Annot" - visual_signature += " /Subtype /Widget" - visual_signature += " /Rect [0 0 0 0]" +// createVisualSignature creates a visual signature field in a PDF document. +// visible: determines if the signature field should be visible or not. +// 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 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\n") + // Specify the annotation subtype as a 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]\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]\n") + } + + // Retrieve the root object from the PDF trailer. root := context.PDFReader.Trailer().Key("Root") + // Get all keys from the root object. root_keys := root.Keys() found_pages := false for _, key := range root_keys { if key == "Pages" { + // Check if the root object contains the "Pages" key. found_pages = true break } } + // Get the pointer to the root object. rootPtr := root.GetPtr() + // Store the root object reference in the catalog data. context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R" if found_pages { - first_page, err := findFirstPage(root.Key("Pages")) + // Find the page object by its number. + page, err := findPageByNumber(root.Key("Pages"), pageNumber) if err != nil { - return "", err + return nil, err } - first_page_ptr := first_page.GetPtr() + // Get the pointer to the page object. + 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.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\n") + } + + // Define the annotation flags for the signature field (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\n") + // Set a unique title for the signature field. + visual_signature.WriteString(fmt.Sprintf(" /T %s\n", pdfString("Signature "+strconv.Itoa(len(context.existingSignatures)+1)))) + + // Reference the signature dictionary. + visual_signature.WriteString(fmt.Sprintf(" /V %d 0 R\n", context.SignData.objectId)) + + // Close the dictionary and end the object. + 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") - visual_signature += " /P " + strconv.Itoa(int(first_page_ptr.GetID())) + " " + strconv.Itoa(int(first_page_ptr.GetGen())) + " R" + // 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())) + } } - visual_signature += " /F 132" - visual_signature += " /FT /Sig" - visual_signature += " /T " + pdfString("Signature") - visual_signature += " /Ff 0" - visual_signature += " /V " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R" + if page.Key("Annots").IsNull() { + page_buffer.WriteString(fmt.Sprintf(" /Annots [%d 0 R]\n", annot)) + } - visual_signature += " >>" - visual_signature += "\nendobj\n" + page_buffer.WriteString(">>\n") - return visual_signature, nil + return page_buffer.Bytes(), nil +} + +// Helper function to find a page by its number. +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++ { + page, err := findPageByNumber(kids.Index(i), pageNumber) + if err == nil { + return page, nil + } + } + } else if pages.Key("Type").Name() == "Page" { + if pageNumber == 1 { + return pages, nil + } + pageNumber-- + } + return pdf.Value{}, fmt.Errorf("page number %d not found", pageNumber) } diff --git a/pkg/provider/pdfsig/pdfxref.go b/pkg/provider/pdfsig/pdfxref.go index 5c0d6ae..89df8f6 100644 --- a/pkg/provider/pdfsig/pdfxref.go +++ b/pkg/provider/pdfsig/pdfxref.go @@ -6,122 +6,251 @@ import ( "encoding/binary" "encoding/hex" "errors" + "fmt" + "io" "strconv" + "strings" ) -func (context *SignContext) writeXref() error { +type xrefEntry struct { + ID uint32 + Offset int64 +} - if context.PDFReader.XrefInformation.Type == "table" { - if err := context.writeXrefTable(); err != nil { - return err - } - } else if context.PDFReader.XrefInformation.Type == "stream" { - if err := context.writeXrefStream(); err != nil { - return err +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) } - } else { - return errors.New("Unkwn xref type: " + context.PDFReader.XrefInformation.Type) + 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, + }) + + 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) writeXrefTable() error { - // @todo: maybe we need a prev here too. - xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) + "\n" - new_xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+4, 10) + "\n" +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", id))); err != nil { + return fmt.Errorf("failed to write object header: %w", err) + } - if _, err := context.OutputBuffer.Write([]byte(new_xref_size)); err != nil { - return err + // Write the object content + object = bytes.TrimSpace(object) + if _, err := context.OutputBuffer.Write(object); err != nil { + return fmt.Errorf("failed to write object content: %w", err) } - // Write the old xref table to the output pdf. - if err := writePartFromSourceFileToTargetFile(context.InputFile, context.OutputBuffer, context.PDFReader.XrefInformation.StartPos+int64(len(xref_size)), context.PDFReader.XrefInformation.Length-int64(len(xref_size))); err != nil { - return err + // Write the object footer + if _, err := context.OutputBuffer.Write([]byte(objectFooter)); err != nil { + return fmt.Errorf("failed to write object footer: %w", err) } - // Create the new catalog xref line. - visual_signature_object_start_position := strconv.FormatInt(context.Filesize, 10) - visual_signature_xref_line := leftPad(visual_signature_object_start_position, "0", 10-len(visual_signature_object_start_position)) + " 00000 n \n" + return nil +} - // Write the new catalog xref line. - if _, err := context.OutputBuffer.Write([]byte(visual_signature_xref_line)); err != nil { - return err +// 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() + case "stream": + return context.writeXrefStream() + default: + return fmt.Errorf("unknown xref type: %s", context.PDFReader.XrefInformation.Type) } +} - // Create the new catalog xref line. - catalog_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length, 10) - catalog_xref_line := leftPad(catalog_object_start_position, "0", 10-len(catalog_object_start_position)) + " 00000 n \n" +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 0, fmt.Errorf("failed to seek to xref table: %w", err) + } - // Write the new catalog xref line. - if _, err := context.OutputBuffer.Write([]byte(catalog_xref_line)); err != nil { - return err + // Read the existing xref table + xrefContent := make([]byte, context.PDFReader.XrefInformation.Length) + if _, err := context.InputFile.Read(xrefContent); err != nil { + return 0, fmt.Errorf("failed to read xref table: %w", err) } - // Create the new signature xref line. - info_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length+context.CatalogData.Length, 10) - info_xref_line := leftPad(info_object_start_position, "0", 10-len(info_object_start_position)) + " 00000 n \n" + // Parse the xref header + xrefLines := strings.Split(string(xrefContent), "\n") + xrefHeader := strings.Fields(xrefLines[1]) + if len(xrefHeader) != 2 { + return 0, fmt.Errorf("invalid xref header format") + } - // Write the new signature xref line. - if _, err := context.OutputBuffer.Write([]byte(info_xref_line)); err != nil { - return err + firstObjectID, err := strconv.ParseUint(xrefHeader[0], 10, 32) + if err != nil { + return 0, fmt.Errorf("invalid first object ID: %w", err) } - // Create the new signature xref line. - signature_object_start_position := strconv.FormatInt(context.Filesize+context.VisualSignData.Length+context.CatalogData.Length+context.InfoData.Length, 10) - signature_xref_line := leftPad(signature_object_start_position, "0", 10-len(signature_object_start_position)) + " 00000 n \n" + itemCount, err := strconv.ParseUint(xrefHeader[1], 10, 32) + if err != nil { + return 0, fmt.Errorf("invalid item count: %w", err) + } - // Write the new signature xref line. - if _, err := context.OutputBuffer.Write([]byte(signature_xref_line)); err != nil { - return err + return uint32(firstObjectID + itemCount), nil +} + +// writeIncrXrefTable writes the incremental cross-reference table to the output buffer. +func (context *SignContext) writeIncrXrefTable() error { + // 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 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 { + return fmt.Errorf("failed to write starting xref object: %w", err) + } + + // Write new entries + 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: %w", err) + } } return nil } +// writeXrefStream writes the cross-reference stream to the output buffer. func (context *SignContext) writeXrefStream() error { - buffer := bytes.NewBuffer(nil) + buffer := new(bytes.Buffer) predictor := context.PDFReader.Trailer().Key("DecodeParms").Key("Predictor").Int64() + if err := writeXrefStreamEntries(buffer, context); err != nil { + return fmt.Errorf("failed to write xref stream entries: %w", err) + } + + streamBytes, err := encodeXrefStream(buffer.Bytes(), predictor) + if err != nil { + return fmt.Errorf("failed to encode xref stream: %w", err) + } + + if err := writeXrefStreamHeader(context, len(streamBytes)); err != nil { + return fmt.Errorf("failed to write xref stream header: %w", err) + } + + if err := writeXrefStreamContent(context, streamBytes); err != nil { + return fmt.Errorf("failed to write xref stream content: %w", err) + } + + return nil +} + +// writeXrefStreamEntries writes the individual entries for the xref stream. +func writeXrefStreamEntries(buffer *bytes.Buffer, context *SignContext) error { + for _, entry := range context.newXrefEntries { + writeXrefStreamLine(buffer, 1, int(entry.Offset), 0) + } + + return nil +} + +// encodeXrefStream applies the appropriate encoding to the xref stream. +func encodeXrefStream(data []byte, predictor int64) ([]byte, error) { var streamBytes []byte var err error - writeXrefStreamLine(buffer, 1, int(context.Filesize), 0) - writeXrefStreamLine(buffer, 1, int(context.Filesize+context.VisualSignData.Length), 0) - writeXrefStreamLine(buffer, 1, int(context.Filesize+context.VisualSignData.Length+context.CatalogData.Length), 0) - writeXrefStreamLine(buffer, 1, int(context.Filesize+context.VisualSignData.Length+context.CatalogData.Length+context.InfoData.Length), 0) - writeXrefStreamLine(buffer, 1, int(context.NewXrefStart), 0) + switch predictor { + case pngSubPredictor: + streamBytes, err = EncodePNGSUBBytes(xrefStreamColumns, data) + case pngUpPredictor: + streamBytes, err = EncodePNGUPBytes(xrefStreamColumns, data) + default: + return nil, fmt.Errorf("unsupported predictor: %d", predictor) + } - // If original uses PNG Sub, use that. - if predictor == 11 { - streamBytes, err = EncodePNGSUBBytes(5, buffer.Bytes()) - if err != nil { - return err - } - } else { - // Do PNG - Up by default. - streamBytes, err = EncodePNGUPBytes(5, buffer.Bytes()) - if err != nil { - return err - } + if err != nil { + return nil, fmt.Errorf("failed to encode xref stream: %w", err) } - new_info := "Info " + strconv.FormatInt(int64(context.InfoData.ObjectId), 10) + " 0 R" - new_root := "Root " + strconv.FormatInt(int64(context.CatalogData.ObjectId), 10) + " 0 R" + return streamBytes, nil +} +// writeXrefStreamHeader writes the header for the xref stream. +func writeXrefStreamHeader(context *SignContext, streamLength int) error { id := context.PDFReader.Trailer().Key("ID") - id0 := hex.EncodeToString([]byte(id.Index(0).RawString())) id1 := hex.EncodeToString([]byte(id.Index(0).RawString())) - new_xref := strconv.Itoa(int(context.SignData.ObjectId+1)) + " 0 obj\n" - new_xref += "<< /Type /XRef /Length " + strconv.Itoa(len(streamBytes)) + " /Filter /FlateDecode /DecodeParms << /Columns 5 /Predictor 12 >> /W [ 1 3 1 ] /Prev " + strconv.FormatInt(context.PDFReader.XrefInformation.StartPos, 10) + " /Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+5, 10) + " /Index [ " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10) + " 5 ] /" + new_info + " /" + new_root + " /ID [<" + id0 + "><" + id1 + ">] >>\n" - if _, err := context.OutputBuffer.Write([]byte(new_xref)); err != nil { - return err - } + 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 +} - if _, err := context.OutputBuffer.Write([]byte("stream\n")); err != nil { +// writeXrefStreamContent writes the content of the xref stream. +func writeXrefStreamContent(context *SignContext, streamBytes []byte) error { + if _, err := io.WriteString(context.OutputBuffer, "stream\n"); err != nil { return err } @@ -129,25 +258,28 @@ func (context *SignContext) writeXrefStream() error { return err } - if _, err := context.OutputBuffer.Write([]byte("\nendstream\n")); err != nil { + if _, err := io.WriteString(context.OutputBuffer, "\nendstream\n"); err != nil { return err } return nil } +// writeXrefStreamLine writes a single line in the xref stream. func writeXrefStreamLine(b *bytes.Buffer, xreftype byte, offset int, gen byte) { b.WriteByte(xreftype) b.Write(encodeInt(offset)) b.WriteByte(gen) } +// encodeInt encodes an integer to a 3-byte slice. func encodeInt(i int) []byte { result := make([]byte, 4) binary.BigEndian.PutUint32(result, uint32(i)) return result[1:4] } +// EncodePNGSUBBytes encodes data using PNG SUB filter. func EncodePNGSUBBytes(columns int, data []byte) ([]byte, error) { rowCount := len(data) / columns if len(data)%columns != 0 { @@ -179,6 +311,7 @@ func EncodePNGSUBBytes(columns int, data []byte) ([]byte, error) { return b.Bytes(), nil } +// EncodePNGUPBytes encodes data using PNG UP filter. func EncodePNGUPBytes(columns int, data []byte) ([]byte, error) { rowCount := len(data) / columns if len(data)%columns != 0 { diff --git a/pkg/provider/pdfsig/sign.go b/pkg/provider/pdfsig/sign.go index 986b7d5..e434a43 100644 --- a/pkg/provider/pdfsig/sign.go +++ b/pkg/provider/pdfsig/sign.go @@ -33,6 +33,8 @@ type TSA struct { Password string } +type CertType uint + type RevocationFunction func(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error type signedData struct { @@ -148,7 +150,6 @@ type SignedData struct { } type SignData struct { - ObjectId uint32 Signature SignDataSignature TPPOpts signers.SignOpts DigestAlgorithm crypto.Hash @@ -157,28 +158,40 @@ type SignData struct { TSA TSA RevocationData revocation.InfoArchival RevocationFunction RevocationFunction + 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 { - ObjectId uint32 - Length int64 + pageObjectId uint32 + objectId uint32 } type InfoData struct { ObjectId uint32 - Length int64 } type SignDataSignature struct { - CertType uint + CertType CertType DocMDPPerm uint Info SignDataSignatureInfo } const ( - CertificationSignature = iota + 1 + CertificationSignature CertType = iota + 1 ApprovalSignature UsageRightsSignature + TimeStampSignature ) const ( @@ -196,7 +209,6 @@ type SignDataSignatureInfo struct { } type SignContext struct { - Filesize int64 InputFile io.ReadSeeker OutputFile io.Writer OutputBuffer *filebuffer.Buffer @@ -211,10 +223,15 @@ type SignContext struct { ByteRangeValues []int64 SignatureMaxLength uint32 SignatureMaxLengthBase uint32 + + existingSignatures []SignData + lastXrefID uint32 + newXrefEntries []xrefEntry + updatedXrefEntries []xrefEntry } -func SignFile(r io.Reader, sd SignData) ([]byte, error) { - input_file, err := os.Open(sd.TPPOpts.Path) +func SignFile(r io.Reader, sign_data SignData) ([]byte, error) { + input_file, err := os.Open(sign_data.TPPOpts.Path) if err != nil { return nil, err } @@ -231,31 +248,26 @@ func SignFile(r io.Reader, sd SignData) ([]byte, error) { return nil, err } - return Sign(input_file, rdr, size, sd) + return Sign(input_file, rdr, size, sign_data) } func Sign(input io.ReadSeeker, rdr *pdf.Reader, size int64, sign_data SignData) ([]byte, error) { - sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3 - - // We do size+1 because we insert a newline. + sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 2 context := SignContext{ - Filesize: size + 1, - PDFReader: rdr, - InputFile: input, - 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, SignData: sign_data, SignatureMaxLengthBase: uint32(hex.EncodedLen(512)), } + // Fetch existing signatures + existingSignatures, err := context.fetchExistingSignatures() + if err != nil { + return nil, err + } + context.existingSignatures = existingSignatures + signedPayload, err := context.SignPDF() if err != nil { return nil, err @@ -275,10 +287,13 @@ func (context *SignContext) SignPDF() ([]byte, 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{}) - // Copy old file into new file. + // Copy old file into new buffer. _, err := context.InputFile.Seek(0, 0) if err != nil { return nil, err @@ -295,48 +310,60 @@ func (context *SignContext) SignPDF() ([]byte, error) { // Base size for signature. context.SignatureMaxLength = context.SignatureMaxLengthBase - switch context.SignData.Certificate.SignatureAlgorithm.String() { - case "SHA1-RSA": - case "ECDSA-SHA1": - case "DSA-SHA1": - context.SignatureMaxLength += uint32(hex.EncodedLen(128)) - case "SHA256-RSA": - case "ECDSA-SHA256": - case "DSA-SHA256": - context.SignatureMaxLength += uint32(hex.EncodedLen(256)) - case "SHA384-RSA": - case "ECDSA-SHA384": - context.SignatureMaxLength += uint32(hex.EncodedLen(384)) - case "SHA512-RSA": - case "ECDSA-SHA512": - context.SignatureMaxLength += uint32(hex.EncodedLen(512)) - } - - // Add size of digest algorithm twice (for file digist and signing certificate attribute) - context.SignatureMaxLength += uint32(hex.EncodedLen(context.SignData.DigestAlgorithm.Size() * 2)) - - // Add size for my certificate. - degenerated, err := pkcs7.DegenerateCertificate(context.SignData.Certificate.Raw) - if err != nil { - return nil, fmt.Errorf("failed to degenerate certificate: %w", err) - } + // If not a timestamp signature + if context.SignData.Signature.CertType != TimeStampSignature { + switch context.SignData.Certificate.SignatureAlgorithm.String() { + case "SHA1-RSA": + case "ECDSA-SHA1": + case "DSA-SHA1": + context.SignatureMaxLength += uint32(hex.EncodedLen(128)) + case "SHA256-RSA": + case "ECDSA-SHA256": + case "DSA-SHA256": + context.SignatureMaxLength += uint32(hex.EncodedLen(256)) + case "SHA384-RSA": + case "ECDSA-SHA384": + context.SignatureMaxLength += uint32(hex.EncodedLen(384)) + case "SHA512-RSA": + case "ECDSA-SHA512": + context.SignatureMaxLength += uint32(hex.EncodedLen(512)) + } - context.SignatureMaxLength += uint32(hex.EncodedLen(len(degenerated))) + // Add size of digest algorithm twice (for file digist and signing certificate attribute) + context.SignatureMaxLength += uint32(hex.EncodedLen(context.SignData.DigestAlgorithm.Size() * 2)) - // Add size for certificate chain. - var certificate_chain []*x509.Certificate - if len(context.SignData.CertificateChains) > 0 && len(context.SignData.CertificateChains[0]) > 1 { - certificate_chain = context.SignData.CertificateChains[0][1:] - } + // Add size for my certificate. + degenerated, err := pkcs7.DegenerateCertificate(context.SignData.Certificate.Raw) + if err != nil { + return nil, fmt.Errorf("failed to degenerate certificate: %w", err) + } + + context.SignatureMaxLength += uint32(hex.EncodedLen(len(degenerated))) + + // Add size of the raw issuer which is added by AddSignerChain + context.SignatureMaxLength += uint32(hex.EncodedLen(len(context.SignData.Certificate.RawIssuer))) - if len(certificate_chain) > 0 { - for _, cert := range certificate_chain { - degenerated, err := pkcs7.DegenerateCertificate(cert.Raw) - if err != nil { - return nil, fmt.Errorf("failed to degenerate certificate in chain: %w", err) + // Add size for certificate chain. + var certificate_chain []*x509.Certificate + if len(context.SignData.CertificateChains) > 0 && len(context.SignData.CertificateChains[0]) > 1 { + certificate_chain = context.SignData.CertificateChains[0][1:] + } + + if len(certificate_chain) > 0 { + for _, cert := range certificate_chain { + degenerated, err := pkcs7.DegenerateCertificate(cert.Raw) + if err != nil { + return nil, fmt.Errorf("failed to degenerate certificate in chain: %w", err) + } + + context.SignatureMaxLength += uint32(hex.EncodedLen(len(degenerated))) } + } - context.SignatureMaxLength += uint32(hex.EncodedLen(len(degenerated))) + // Fetch revocation data before adding signature placeholder. + // Revocation data can be quite large and we need to create enough space in the placeholder. + if err := context.fetchRevocationData(); err != nil { + return nil, fmt.Errorf("failed to fetch revocation data: %w", err) } } @@ -349,84 +376,93 @@ func (context *SignContext) SignPDF() ([]byte, error) { context.SignatureMaxLength += uint32(hex.EncodedLen(9000)) } - // Fetch revocation data before adding signature placeholder. - // Revocation data can be quite large and we need to create enough space in the placeholder. - if err := context.fetchRevocationData(); err != nil { - return nil, fmt.Errorf("failed to fetch revocation data: %w", err) + // Create the signature object + var signature_object []byte + + switch context.SignData.Signature.CertType { + case TimeStampSignature: + signature_object = context.createTimestampPlaceholder() + default: + signature_object = context.createSignaturePlaceholder() } - visual_signature, err := context.createVisualSignature() + // Write the new signature object + context.SignData.objectId, err = context.addObject(signature_object) if err != nil { - return nil, fmt.Errorf("failed to create visual signature: %w", err) + return nil, fmt.Errorf("failed to add signature object: %w", err) + } + + // Create visual signature (visible or invisible based on CertType) + visible := false + rectangle := [4]float64{0, 0, 0, 0} + if context.SignData.Signature.CertType != ApprovalSignature && context.SignData.Appearance.Visible { + return nil, 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, + } } - context.VisualSignData.Length = int64(len(visual_signature)) - - // Write the new catalog object. - if _, err := context.OutputBuffer.Write([]byte(visual_signature)); err != nil { - return nil, err + // Example usage: passing page number and default rect values + visual_signature, err := context.createVisualSignature(visible, context.SignData.Appearance.Page, rectangle) + if err != nil { + return nil, fmt.Errorf("failed to create visual signature: %w", err) } - catalog, err := context.createCatalog() + // Write the new visual signature object. + context.VisualSignData.objectId, err = context.addObject(visual_signature) if err != nil { - return nil, fmt.Errorf("failed to create catalog: %w", err) + return nil, fmt.Errorf("failed to add visual signature object: %w", err) } - context.CatalogData.Length = int64(len(catalog)) - - // Write the new catalog object. - if _, err := context.OutputBuffer.Write([]byte(catalog)); err != nil { - return nil, fmt.Errorf("failed to write catalog: %w", err) + if context.SignData.Appearance.Visible { + inc_page_update, err := context.createIncPageUpdate(context.SignData.Appearance.Page, context.VisualSignData.objectId) + if err != nil { + return nil, fmt.Errorf("failed to create incremental page update: %w", err) + } + err = context.updateObject(context.VisualSignData.pageObjectId, inc_page_update) + if err != nil { + return nil, fmt.Errorf("failed to add incremental page update object: %w", err) + } } - // Create the signature object - signature_object, byte_range_start_byte, signature_contents_start_byte := context.createSignaturePlaceholder() - - info, err := context.createInfo() + // Create a new catalog object + catalog, err := context.createCatalog() if err != nil { - return nil, fmt.Errorf("failed to create info: %w", err) - } - - context.InfoData.Length = int64(len(info)) - - // Write the new catalog object. - if _, err := context.OutputBuffer.Write([]byte(info)); err != nil { - return nil, fmt.Errorf("failed to write info: %w", err) + return nil, fmt.Errorf("failed to create catalog: %w", err) } - appended_bytes := context.Filesize + int64(len(catalog)) + int64(len(visual_signature)) + int64(len(info)) - - // 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 nil, fmt.Errorf("failed to create the new signature object: %w", err) + // Write the new catalog object + context.CatalogData.ObjectId, err = context.addObject(catalog) + if err != nil { + return nil, fmt.Errorf("failed to add catalog 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 nil, fmt.Errorf("failed to write xref: %w", err) } + // Write trailer if err := context.writeTrailer(); err != nil { return nil, fmt.Errorf("failed to write trailer: %w", err) } + // Update byte range if err := context.updateByteRange(); err != nil { return nil, fmt.Errorf("failed to update byte range: %w", err) } + // Replace signature if _, err := context.replaceSignature(); err != nil { return nil, fmt.Errorf("failed to replace signature: %w", err) } + // Write final output if _, err := context.OutputBuffer.Seek(0, 0); err != nil { return nil, err } @@ -434,7 +470,7 @@ func (context *SignContext) SignPDF() ([]byte, error) { /* if _, err := context.OutputFile.Write(file_content); err != nil { - return nil, fmt.Errorf("failed to write to output file: %w", err) + return nil, err }*/ return file_content, nil