Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial pdf visual signature support #176

Merged
merged 1 commit into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading