Skip to content

Commit

Permalink
Merge pull request #176 from Venafi/pdf-visual-sig
Browse files Browse the repository at this point in the history
initial pdf visual signature support
  • Loading branch information
zosocanuck authored Dec 31, 2024
2 parents 9321a7b + 5bda988 commit 7c318c4
Show file tree
Hide file tree
Showing 15 changed files with 1,101 additions and 397 deletions.
13 changes: 12 additions & 1 deletion EXPERIMENTAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
```

#### 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 "[email protected]" --visual
```
2 changes: 2 additions & 0 deletions cmd/vsign/cli/options/pdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type PDFOptions struct {
Reason string
Contact string
TSA string
Visual bool
}

var _ Interface = (*PDFOptions)(nil)
Expand All @@ -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", "[email protected]", "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")

}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
30 changes: 29 additions & 1 deletion pkg/plugin/signers/pdf/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -49,6 +50,7 @@ func init() {
PDFSigner.Flags().String("reason", "Contract", "Reason for signing")
PDFSigner.Flags().String("contact", "[email protected]", "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)
}

Expand All @@ -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{
Expand All @@ -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,
Expand Down
62 changes: 62 additions & 0 deletions pkg/provider/pdfsig/appearance.go
Original file line number Diff line number Diff line change
@@ -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
}
25 changes: 25 additions & 0 deletions pkg/provider/pdfsig/certtype_string.go
Original file line number Diff line number Diff line change
@@ -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]]
}
3 changes: 2 additions & 1 deletion pkg/provider/pdfsig/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
60 changes: 32 additions & 28 deletions pkg/provider/pdfsig/pdfbyterange.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pdfsig

import (
"bytes"
"fmt"
"strings"
)
Expand All @@ -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
}

Expand Down
Loading

0 comments on commit 7c318c4

Please sign in to comment.