Merged feature/pdf-signer into master
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.idea
|
||||||
|
*.pdf
|
||||||
|
*.pdf.*
|
||||||
|
!testfile*.pdf
|
||||||
|
testfiles/*_signed.pdf
|
||||||
|
testfiles/failed/*
|
||||||
|
pdfsign
|
||||||
|
certs/*
|
@@ -1,19 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDBDCCAm2gAwIBAgIJAP6vkkLP72OOMA0GCSqGSIb3DQEBCwUAMIGZMQswCQYD
|
|
||||||
VQQGEwJOTDEVMBMGA1UECAwMWnVpZC1Ib2xsYW5kMRIwEAYDVQQHDAlSb3R0ZXJk
|
|
||||||
YW0xEjAQBgNVBAoMCVVuaWNvZGVyczELMAkGA1UECwwCSVQxGjAYBgNVBAMMEUpl
|
|
||||||
cm9lbiBCb2JiZWxkaWprMSIwIAYJKoZIhvcNAQkBFhNqZXJvZW5AdW5pY29kZXJz
|
|
||||||
Lm5sMCAXDTE3MDcwNjE5MzYwOVoYDzMwMTYxMTA2MTkzNjA5WjCBmTELMAkGA1UE
|
|
||||||
BhMCTkwxFTATBgNVBAgMDFp1aWQtSG9sbGFuZDESMBAGA1UEBwwJUm90dGVyZGFt
|
|
||||||
MRIwEAYDVQQKDAlVbmljb2RlcnMxCzAJBgNVBAsMAklUMRowGAYDVQQDDBFKZXJv
|
|
||||||
ZW4gQm9iYmVsZGlqazEiMCAGCSqGSIb3DQEJARYTamVyb2VuQHVuaWNvZGVycy5u
|
|
||||||
bDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArpfaVUltYdOSISuc8V5vAy6b
|
|
||||||
jpqYuxsS5I6jpL1nMKms9IB5+uk+Glo2O/tb+W/R8zxQ3xrQ6JWZ4ZSsBhKNVink
|
|
||||||
Su3+kdAQJfHn3NLJzx0QGceo0TF2RvVGo5c91zxuA8rchdNz1QxrD6QesGKyfsXn
|
|
||||||
F+oELezafT346PbeqikCAwEAAaNQME4wHQYDVR0OBBYEFKA68BB0iwhY2RIRFIYs
|
|
||||||
gmq0l6y7MB8GA1UdIwQYMBaAFKA68BB0iwhY2RIRFIYsgmq0l6y7MAwGA1UdEwQF
|
|
||||||
MAMBAf8wDQYJKoZIhvcNAQELBQADgYEAZ75HjcE/d/nclPTQbCN9qvUyuU76ml4O
|
|
||||||
jDN8T+loOsUKmI4VVsNLzF6DXq8sg4EP7s8kEEzM7qhoijw09OUhVniBYN3SzJYX
|
|
||||||
l8AiThPGqcIm1TrkqPULYQBu/FnMoL6SP7kAULcsUvEmn1rPcG9ESQ4sK/ceJhFZ
|
|
||||||
zk9o3rVC0PU=
|
|
||||||
-----END CERTIFICATE-----
|
|
145
p11sign.go
145
p11sign.go
@@ -1,145 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"crypto/x509"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"bitbucket.org/digitorus/pdfsign/revocation"
|
|
||||||
"bitbucket.org/digitorus/pdfsign/sign"
|
|
||||||
"bitbucket.org/digitorus/pdfsign/verify"
|
|
||||||
"bitbucket.org/digitorus/pkcs11"
|
|
||||||
)
|
|
||||||
|
|
||||||
func usage() {
|
|
||||||
log.Fatal("Usage: sign input.pdf output.pdf pkcs11-password [chain.crt] OR verify input.pdf")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if len(flag.Args()) < 2 {
|
|
||||||
usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
method := flag.Arg(0)
|
|
||||||
if method != "sign" && method != "verify" {
|
|
||||||
usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
input := flag.Arg(1)
|
|
||||||
if len(input) == 0 {
|
|
||||||
usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == "verify" {
|
|
||||||
input_file, err := os.Open(input)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer input_file.Close()
|
|
||||||
|
|
||||||
resp, err := verify.Verify(input_file)
|
|
||||||
log.Println(resp)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == "sign" {
|
|
||||||
if len(flag.Args()) < 4 {
|
|
||||||
usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
output := flag.Arg(2)
|
|
||||||
if len(output) == 0 {
|
|
||||||
usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkcs11 key
|
|
||||||
lib, err := pkcs11.FindLib("/lib64/libeTPkcs11.so")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Library
|
|
||||||
ctx := pkcs11.New(lib)
|
|
||||||
if ctx == nil {
|
|
||||||
log.Fatal("Failed to load library")
|
|
||||||
}
|
|
||||||
err = ctx.Initialize()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
// login
|
|
||||||
session, err := pkcs11.CreateSession(ctx, 0, flag.Arg(3), false)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
// select the first certificate
|
|
||||||
cert, ckaId, err := pkcs11.GetCert(ctx, session, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// private key
|
|
||||||
pkey, err := pkcs11.InitPrivateKey(ctx, session, ckaId)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certificate_chains := make([][]*x509.Certificate, 0)
|
|
||||||
|
|
||||||
if flag.Arg(4) != "" {
|
|
||||||
certificate_pool := x509.NewCertPool()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chain_data, err := ioutil.ReadFile(flag.Arg(4))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certificate_pool.AppendCertsFromPEM(chain_data)
|
|
||||||
certificate_chains, err = cert.Verify(x509.VerifyOptions{
|
|
||||||
Intermediates: certificate_pool,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Obtain TSA from certificate or CLI
|
|
||||||
err = sign.SignFile(input, output, sign.SignData{
|
|
||||||
Signature: sign.SignDataSignature{
|
|
||||||
Info: sign.SignDataSignatureInfo{
|
|
||||||
Name: "Jeroen Bobbeldijk",
|
|
||||||
Location: "Rotterdam",
|
|
||||||
Reason: "Test",
|
|
||||||
ContactInfo: "Geen",
|
|
||||||
Date: time.Now().Local(),
|
|
||||||
},
|
|
||||||
CertType: 2,
|
|
||||||
Approval: false,
|
|
||||||
},
|
|
||||||
Signer: pkey,
|
|
||||||
Certificate: cert,
|
|
||||||
CertificateChains: certificate_chains,
|
|
||||||
TSA: sign.TSA{
|
|
||||||
URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23",
|
|
||||||
},
|
|
||||||
RevocationData: revocation.InfoArchival{},
|
|
||||||
RevocationFunction: sign.DefaultEmbedRevocationStatusFunction,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
} else {
|
|
||||||
log.Println("Signed PDF written to " + output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
2
sign.go
2
sign.go
@@ -130,7 +130,7 @@ func main() {
|
|||||||
Certificate: cert,
|
Certificate: cert,
|
||||||
CertificateChains: certificate_chains,
|
CertificateChains: certificate_chains,
|
||||||
TSA: sign.TSA{
|
TSA: sign.TSA{
|
||||||
URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23",
|
URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23",
|
||||||
},
|
},
|
||||||
RevocationData: revocation.InfoArchival{},
|
RevocationData: revocation.InfoArchival{},
|
||||||
RevocationFunction: sign.DefaultEmbedRevocationStatusFunction,
|
RevocationFunction: sign.DefaultEmbedRevocationStatusFunction,
|
||||||
|
@@ -1,21 +1,28 @@
|
|||||||
package sign
|
package sign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bitbucket.org/digitorus/pdf"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"bitbucket.org/digitorus/pdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findFirstPage(parent pdf.Value) (pdf.Value, error) {
|
func findFirstPage(parent pdf.Value) (pdf.Value, error) {
|
||||||
value_type := parent.Key("Type").String()
|
value_type := parent.Key("Type").String()
|
||||||
if value_type == "/Pages" {
|
if value_type == "/Pages" {
|
||||||
recurse_parent, recurse_err := findFirstPage(parent.Key("Kids").Index(0))
|
|
||||||
return recurse_parent, recurse_err
|
for i := 0; i < parent.Key("Kids").Len(); i++ {
|
||||||
|
recurse_parent, recurse_err := findFirstPage(parent.Key("Kids").Index(i))
|
||||||
|
if recurse_err == nil {
|
||||||
|
return recurse_parent, recurse_err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent, errors.New("Could not find first page.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if value_type == "/Page" {
|
if value_type == "/Page" {
|
||||||
@@ -66,10 +73,13 @@ func pdfDateTime(date time.Time) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func leftPad(s string, padStr string, pLen int) string {
|
func leftPad(s string, padStr string, pLen int) string {
|
||||||
|
if pLen <= 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
return strings.Repeat(padStr, pLen) + s
|
return strings.Repeat(padStr, pLen) + s
|
||||||
}
|
}
|
||||||
|
|
||||||
func writePartFromSourceFileToTargetFile(input_file *os.File, output_file *os.File, offset int64, length int64) error {
|
func writePartFromSourceFileToTargetFile(input_file io.ReadSeeker, output_file io.Writer, offset int64, length int64) error {
|
||||||
input_file.Seek(offset, 0)
|
input_file.Seek(offset, 0)
|
||||||
|
|
||||||
// Create a small buffer for proper IO handling.
|
// Create a small buffer for proper IO handling.
|
||||||
@@ -83,6 +93,10 @@ func writePartFromSourceFileToTargetFile(input_file *os.File, output_file *os.Fi
|
|||||||
// Track read/written bytes so we know when we're done.
|
// Track read/written bytes so we know when we're done.
|
||||||
read_bytes := int64(0)
|
read_bytes := int64(0)
|
||||||
|
|
||||||
|
if length <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Create a buffer for the chunks.
|
// Create a buffer for the chunks.
|
||||||
buf := make([]byte, max_chunk_length)
|
buf := make([]byte, max_chunk_length)
|
||||||
for {
|
for {
|
||||||
|
183
sign/helpers_test.go
Normal file
183
sign/helpers_test.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bitbucket.org/digitorus/pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindFirstPage(t *testing.T) {
|
||||||
|
input_file, reader := loadHelpersTestPDF()
|
||||||
|
if input_file == nil || reader == nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root := reader.Trailer().Key("Root")
|
||||||
|
root_keys := root.Keys()
|
||||||
|
found_pages := false
|
||||||
|
for _, key := range root_keys {
|
||||||
|
if key == "Pages" {
|
||||||
|
found_pages = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found_pages {
|
||||||
|
input_file.Close()
|
||||||
|
t.Errorf("Could not find pages element")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
first_page, err := findFirstPage(root.Key("Pages"))
|
||||||
|
if err != nil {
|
||||||
|
input_file.Close()
|
||||||
|
t.Errorf("Could not find first page")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
first_page_ptr := first_page.GetPtr()
|
||||||
|
if first_page_ptr.GetGen() != 0 {
|
||||||
|
input_file.Close()
|
||||||
|
t.Errorf("First page gen mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
if first_page_ptr.GetID() != 4 {
|
||||||
|
t.Errorf("First page ID mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
input_file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPDFString(t *testing.T) {
|
||||||
|
string_compare := map[string]string{
|
||||||
|
"Test": "(Test)",
|
||||||
|
"((Test)": "(\\(\\(Test\\))",
|
||||||
|
"\\TEst": "(\\\\TEst)",
|
||||||
|
"\rnew": "(\\rnew)",
|
||||||
|
}
|
||||||
|
|
||||||
|
for text, expected := range string_compare {
|
||||||
|
if pdfString(text) != expected {
|
||||||
|
t.Errorf("Error while escaping %s. Expected %s, got %s.", text, expected, pdfString(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPdfDateTime(t *testing.T) {
|
||||||
|
timezone, _ := time.LoadLocation("Europe/Tallinn")
|
||||||
|
timezone_1, _ := time.LoadLocation("Africa/Casablanca")
|
||||||
|
timezone_2, _ := time.LoadLocation("America/New_York")
|
||||||
|
timezone_3, _ := time.LoadLocation("Asia/Jerusalem")
|
||||||
|
timezone_4, _ := time.LoadLocation("Europe/Amsterdam")
|
||||||
|
timezone_5, _ := time.LoadLocation("Pacific/Honolulu")
|
||||||
|
|
||||||
|
now := time.Date(2017, 9, 23, 14, 39, 0, 0, timezone)
|
||||||
|
|
||||||
|
date_compare := map[time.Time]string{
|
||||||
|
now.In(timezone_1): "(D:20170923123900+01'00')",
|
||||||
|
now.In(timezone_2): "(D:20170923073900-04'00')",
|
||||||
|
now.In(timezone_3): "(D:20170923143900+03'00')",
|
||||||
|
now.In(timezone_4): "(D:20170923133900+02'00')",
|
||||||
|
now.In(timezone_5): "(D:20170923013900-10'00')",
|
||||||
|
}
|
||||||
|
|
||||||
|
for date, expected := range date_compare {
|
||||||
|
if pdfDateTime(date) != expected {
|
||||||
|
t.Errorf("Error while converting date %s to string. Expected %s, got %s.", date.String(), expected, pdfDateTime(date))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLeftPad(t *testing.T) {
|
||||||
|
string_compare := map[string]string{
|
||||||
|
"123456789": "123456789",
|
||||||
|
"12345678": "12345678",
|
||||||
|
"1234567": "_1234567",
|
||||||
|
"123456": "__123456",
|
||||||
|
"12345": "___12345",
|
||||||
|
"1234": "____1234",
|
||||||
|
"123": "_____123",
|
||||||
|
"12": "______12",
|
||||||
|
"1": "_______1",
|
||||||
|
"": "________",
|
||||||
|
}
|
||||||
|
|
||||||
|
for text, expected := range string_compare {
|
||||||
|
if leftPad(text, "_", 8-len(text)) != expected {
|
||||||
|
t.Errorf("Error while left padding %s. Expected %s, got %s.", text, expected, leftPad(text, "_", 8-len(text)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWritePartFromSourceFileToTargetFile(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
writer := bufio.NewWriter(&b)
|
||||||
|
|
||||||
|
input_file, err := os.Open("../testfiles/testfile20.pdf")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writePartFromSourceFileToTargetFile(input_file, writer, 0, 0)
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
if writer.Buffered() != 0 {
|
||||||
|
t.Errorf("Content was copied while length was 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
writePartFromSourceFileToTargetFile(input_file, writer, 0, -20)
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
if writer.Buffered() != 0 {
|
||||||
|
t.Errorf("Content was copied while length was smaller than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
writePartFromSourceFileToTargetFile(input_file, writer, 0, 8)
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
if string(b.Bytes()) != "%PDF-2.0" {
|
||||||
|
t.Errorf("Wrong content was copied, got %s but expected %s", string(b.Bytes()), "%PDF-2.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
writePartFromSourceFileToTargetFile(input_file, writer, 33, 8)
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
if string(b.Bytes()) != "%PDF-2.0/Catalog" {
|
||||||
|
t.Errorf("Wrong content was copied, got %s but expected %s", string(b.Bytes()), "%PDF-2.0/Catalog")
|
||||||
|
}
|
||||||
|
|
||||||
|
writePartFromSourceFileToTargetFile(input_file, writer, 0, 1200)
|
||||||
|
|
||||||
|
if writer.Buffered() != 1200 {
|
||||||
|
t.Errorf("Requested 1200 bytes but only got %d", writer.Buffered())
|
||||||
|
}
|
||||||
|
|
||||||
|
input_file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadHelpersTestPDF() (*os.File, *pdf.Reader) {
|
||||||
|
input_file, err := os.Open("../testfiles/testfile20.pdf")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
finfo, err := input_file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
input_file.Close()
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
size := finfo.Size()
|
||||||
|
|
||||||
|
rdr, err := pdf.NewReader(input_file, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return input_file, rdr
|
||||||
|
}
|
@@ -6,11 +6,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (context *SignContext) updateByteRange() error {
|
func (context *SignContext) updateByteRange() error {
|
||||||
// Get current filesize. Easier than what should be the current size.
|
context.OutputBuffer.Seek(0, 0)
|
||||||
// @todo: find out of this is safe.
|
output_file_size := int64(context.OutputBuffer.Buff.Len())
|
||||||
output_file_stat, _ := context.OutputFile.Stat()
|
|
||||||
|
|
||||||
output_file_size := output_file_stat.Size()
|
|
||||||
|
|
||||||
// Calculate ByteRange values to replace them.
|
// Calculate ByteRange values to replace them.
|
||||||
context.ByteRangeValues = make([]int64, 4)
|
context.ByteRangeValues = make([]int64, 4)
|
||||||
@@ -32,13 +29,17 @@ func (context *SignContext) updateByteRange() error {
|
|||||||
// Make sure our ByteRange string didn't shrink in length.
|
// Make sure our ByteRange string didn't shrink in length.
|
||||||
new_byte_range += strings.Repeat(" ", len(signatureByteRangePlaceholder)-len(new_byte_range))
|
new_byte_range += strings.Repeat(" ", len(signatureByteRangePlaceholder)-len(new_byte_range))
|
||||||
|
|
||||||
// Seek to ByteRange position in file.
|
context.OutputBuffer.Seek(0, 0)
|
||||||
context.OutputFile.Seek(context.ByteRangeStartByte, 0)
|
file_content := context.OutputBuffer.Buff.Bytes()
|
||||||
|
|
||||||
|
context.OutputBuffer.Write(file_content[:context.ByteRangeStartByte])
|
||||||
|
|
||||||
// Write new ByteRange.
|
// Write new ByteRange.
|
||||||
if _, err := context.OutputFile.Write([]byte(new_byte_range)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(new_byte_range)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.OutputBuffer.Write(file_content[context.ByteRangeStartByte+int64(len(new_byte_range)):])
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package sign
|
package sign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,15 +19,14 @@ func (context *SignContext) createCatalog() (catalog string, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found_pages {
|
|
||||||
return "", errors.New("Didn't find pages in PDF trailer Root.")
|
|
||||||
}
|
|
||||||
|
|
||||||
rootPtr := root.GetPtr()
|
rootPtr := root.GetPtr()
|
||||||
context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R"
|
context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R"
|
||||||
|
|
||||||
pages := root.Key("Pages").GetPtr()
|
if found_pages {
|
||||||
catalog += " /Pages " + strconv.Itoa(int(pages.GetID())) + " " + strconv.Itoa(int(pages.GetGen())) + " R"
|
pages := root.Key("Pages").GetPtr()
|
||||||
|
catalog += " /Pages " + strconv.Itoa(int(pages.GetID())) + " " + strconv.Itoa(int(pages.GetGen())) + " R"
|
||||||
|
}
|
||||||
|
|
||||||
catalog += " /AcroForm <<"
|
catalog += " /AcroForm <<"
|
||||||
catalog += " /Fields [" + strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 R]"
|
catalog += " /Fields [" + strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 R]"
|
||||||
|
|
||||||
|
56
sign/pdfcatalog_test.go
Normal file
56
sign/pdfcatalog_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"bitbucket.org/digitorus/pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateCatalog(t *testing.T) {
|
||||||
|
input_file, err := os.Open("../testfiles/testfile20.pdf")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finfo, err := input_file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := finfo.Size()
|
||||||
|
|
||||||
|
rdr, err := pdf.NewReader(input_file, size)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
context := SignContext{
|
||||||
|
Filesize: size + 1,
|
||||||
|
PDFReader: rdr,
|
||||||
|
InputFile: input_file,
|
||||||
|
VisualSignData: VisualSignData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount),
|
||||||
|
},
|
||||||
|
CatalogData: CatalogData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
|
||||||
|
},
|
||||||
|
InfoData: InfoData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog, err := context.createCatalog()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_catalog := "11 0 obj\n<< /Type /Catalog /Version /2.0 /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 1 >> /Perms << /UR3 0 0 R >> >>\nendobj\n"
|
||||||
|
|
||||||
|
if catalog != expected_catalog {
|
||||||
|
t.Errorf("Catalog mismatch, expected %s, but got %s", expected_catalog, catalog)
|
||||||
|
}
|
||||||
|
}
|
138
sign/pdfinfo_test.go
Normal file
138
sign/pdfinfo_test.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"bitbucket.org/digitorus/pdf"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateInfoEmpty(t *testing.T) {
|
||||||
|
input_file, err := os.Open("../testfiles/testfile20.pdf")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finfo, err := input_file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := finfo.Size()
|
||||||
|
|
||||||
|
rdr, err := pdf.NewReader(input_file, size)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_data := SignData{
|
||||||
|
Signature: SignDataSignature{
|
||||||
|
Info: SignDataSignatureInfo{
|
||||||
|
Name: "Jeroen Bobbeldijk",
|
||||||
|
Location: "Rotterdam",
|
||||||
|
Reason: "Test",
|
||||||
|
ContactInfo: "Geen",
|
||||||
|
Date: time.Now().Local(),
|
||||||
|
},
|
||||||
|
CertType: 2,
|
||||||
|
Approval: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3
|
||||||
|
|
||||||
|
context := SignContext{
|
||||||
|
Filesize: size + 1,
|
||||||
|
PDFReader: rdr,
|
||||||
|
InputFile: input_file,
|
||||||
|
VisualSignData: VisualSignData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount),
|
||||||
|
},
|
||||||
|
CatalogData: CatalogData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
|
||||||
|
},
|
||||||
|
InfoData: InfoData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
|
||||||
|
},
|
||||||
|
SignData: sign_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := context.createInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_info := "12 0 obj\n<<>>\nendobj\n"
|
||||||
|
if info != expected_info {
|
||||||
|
t.Errorf("Info mismatch, expected %s, but got %s", expected_info, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateInfo(t *testing.T) {
|
||||||
|
input_file, err := os.Open("../testfiles/testfile12.pdf")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finfo, err := input_file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := finfo.Size()
|
||||||
|
|
||||||
|
rdr, err := pdf.NewReader(input_file, size)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_data := SignData{
|
||||||
|
Signature: SignDataSignature{
|
||||||
|
Info: SignDataSignatureInfo{
|
||||||
|
Name: "Jeroen Bobbeldijk",
|
||||||
|
Location: "Rotterdam",
|
||||||
|
Reason: "Test",
|
||||||
|
ContactInfo: "Geen",
|
||||||
|
Date: time.Now().Local(),
|
||||||
|
},
|
||||||
|
CertType: 2,
|
||||||
|
Approval: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3
|
||||||
|
|
||||||
|
context := SignContext{
|
||||||
|
Filesize: size + 1,
|
||||||
|
PDFReader: rdr,
|
||||||
|
InputFile: input_file,
|
||||||
|
VisualSignData: VisualSignData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount),
|
||||||
|
},
|
||||||
|
CatalogData: CatalogData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
|
||||||
|
},
|
||||||
|
InfoData: InfoData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
|
||||||
|
},
|
||||||
|
SignData: sign_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := context.createInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_info := "18 0 obj\n<</Author(User: Isamu Ohzawa [isamu])/CreationDate(D:19981025161109)/Creator(FastIO Systems: cover.c)/Keywords(ClibPDF, ANSI C Library, Acrobat, PDF, Dynamic Web, Graph, Plot)/Producer([ClibPDF Library 0.96] NEXTSTEP or OPENSTEP)/Subject(ANSI C Library for Direct PDF Generation)/Title(ClibPDF Reference Manual)>>\nendobj\n"
|
||||||
|
|
||||||
|
if info != expected_info {
|
||||||
|
t.Errorf("Info mismatch, expected %s, but got %s", expected_info, info)
|
||||||
|
}
|
||||||
|
}
|
@@ -2,17 +2,15 @@ package sign
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"crypto/x509"
|
|
||||||
"github.com/digitorus/pkcs7"
|
"github.com/digitorus/pkcs7"
|
||||||
"github.com/digitorus/timestamp"
|
"github.com/digitorus/timestamp"
|
||||||
)
|
)
|
||||||
@@ -31,60 +29,69 @@ type TSAResponse struct {
|
|||||||
|
|
||||||
var signatureByteRangePlaceholder = "/ByteRange[0 ********** ********** **********]"
|
var signatureByteRangePlaceholder = "/ByteRange[0 ********** ********** **********]"
|
||||||
|
|
||||||
func (context *SignContext) createSignaturePlaceholder() (signature string, byte_range_start_byte int64, signature_contents_start_byte int64) {
|
func (context *SignContext) createSignaturePlaceholder() (dssd string, byte_range_start_byte int64, signature_contents_start_byte int64) {
|
||||||
signature = strconv.Itoa(int(context.SignData.ObjectId)) + " 0 obj\n"
|
// Using a buffer because it's way faster than concatenating.
|
||||||
signature += "<< /Type /Sig"
|
var signature_buffer bytes.Buffer
|
||||||
signature += " /Filter /Adobe.PPKLite"
|
signature_buffer.WriteString(strconv.Itoa(int(context.SignData.ObjectId)) + " 0 obj\n")
|
||||||
signature += " /SubFilter /adbe.pkcs7.detached"
|
signature_buffer.WriteString("<< /Type /Sig")
|
||||||
|
signature_buffer.WriteString(" /Filter /Adobe.PPKLite")
|
||||||
|
signature_buffer.WriteString(" /SubFilter /adbe.pkcs7.detached")
|
||||||
|
|
||||||
byte_range_start_byte = int64(len(signature)) + 1
|
byte_range_start_byte = int64(signature_buffer.Len()) + 1
|
||||||
|
|
||||||
// Create a placeholder for the byte range string, we will replace it later.
|
// Create a placeholder for the byte range string, we will replace it later.
|
||||||
signature += " " + signatureByteRangePlaceholder
|
signature_buffer.WriteString(" " + signatureByteRangePlaceholder)
|
||||||
|
|
||||||
signature_contents_start_byte = int64(len(signature)) + 11
|
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 wil replace it later.
|
||||||
signature += " /Contents<" + strings.Repeat("0", int(context.SignatureMaxLength)) + ">"
|
signature_buffer.WriteString(" /Contents<")
|
||||||
|
signature_buffer.Write(bytes.Repeat([]byte("0"), int(context.SignatureMaxLength)))
|
||||||
|
signature_buffer.WriteString(">")
|
||||||
|
|
||||||
if !context.SignData.Signature.Approval {
|
if !context.SignData.Signature.Approval {
|
||||||
signature += " /Reference [" // array of signature reference dictionaries
|
signature_buffer.WriteString(" /Reference [") // array of signature reference dictionaries
|
||||||
signature += " << /Type /SigRef"
|
signature_buffer.WriteString(" << /Type /SigRef")
|
||||||
if context.SignData.Signature.CertType > 0 {
|
if context.SignData.Signature.CertType > 0 {
|
||||||
signature += " /TransformMethod /DocMDP"
|
signature_buffer.WriteString(" /TransformMethod /DocMDP")
|
||||||
signature += " /TransformParams <<"
|
signature_buffer.WriteString(" /TransformParams <<")
|
||||||
signature += " /Type /TransformParams"
|
signature_buffer.WriteString(" /Type /TransformParams")
|
||||||
signature += " /P " + strconv.Itoa(int(context.SignData.Signature.CertType))
|
signature_buffer.WriteString(" /P " + strconv.Itoa(int(context.SignData.Signature.CertType)))
|
||||||
signature += " /V /1.2"
|
signature_buffer.WriteString(" /V /1.2")
|
||||||
} else {
|
} else {
|
||||||
signature += " /TransformMethod /UR3"
|
signature_buffer.WriteString(" /TransformMethod /UR3")
|
||||||
signature += " /TransformParams <<"
|
signature_buffer.WriteString(" /TransformParams <<")
|
||||||
signature += " /Type /TransformParams"
|
signature_buffer.WriteString(" /Type /TransformParams")
|
||||||
signature += " /V /2.2"
|
signature_buffer.WriteString(" /V /2.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
signature += " >>" // close TransformParams
|
signature_buffer.WriteString(" >>") // close TransformParams
|
||||||
signature += " >>"
|
signature_buffer.WriteString(" >>")
|
||||||
signature += " ]" // end of reference
|
signature_buffer.WriteString(" ]") // end of reference
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.SignData.Signature.Info.Name != "" {
|
if context.SignData.Signature.Info.Name != "" {
|
||||||
signature += " /Name " + pdfString(context.SignData.Signature.Info.Name)
|
signature_buffer.WriteString(" /Name ")
|
||||||
|
signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.Name))
|
||||||
}
|
}
|
||||||
if context.SignData.Signature.Info.Location != "" {
|
if context.SignData.Signature.Info.Location != "" {
|
||||||
signature += " /Location " + pdfString(context.SignData.Signature.Info.Location)
|
signature_buffer.WriteString(" /Location ")
|
||||||
|
signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.Location))
|
||||||
}
|
}
|
||||||
if context.SignData.Signature.Info.Reason != "" {
|
if context.SignData.Signature.Info.Reason != "" {
|
||||||
signature += " /Reason " + pdfString(context.SignData.Signature.Info.Reason)
|
signature_buffer.WriteString(" /Reason ")
|
||||||
|
signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.Reason))
|
||||||
}
|
}
|
||||||
if context.SignData.Signature.Info.ContactInfo != "" {
|
if context.SignData.Signature.Info.ContactInfo != "" {
|
||||||
signature += " /ContactInfo " + pdfString(context.SignData.Signature.Info.ContactInfo)
|
signature_buffer.WriteString(" /ContactInfo ")
|
||||||
|
signature_buffer.WriteString(pdfString(context.SignData.Signature.Info.ContactInfo))
|
||||||
}
|
}
|
||||||
signature += " /M " + pdfDateTime(context.SignData.Signature.Info.Date)
|
signature_buffer.WriteString(" /M ")
|
||||||
signature += " >>"
|
signature_buffer.WriteString(pdfDateTime(context.SignData.Signature.Info.Date))
|
||||||
signature += "\nendobj\n"
|
signature_buffer.WriteString(" >>")
|
||||||
|
signature_buffer.WriteString("\nendobj\n")
|
||||||
|
|
||||||
return signature, byte_range_start_byte, signature_contents_start_byte
|
return signature_buffer.String(), byte_range_start_byte, signature_contents_start_byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (context *SignContext) fetchRevocationData() error {
|
func (context *SignContext) fetchRevocationData() error {
|
||||||
@@ -111,22 +118,20 @@ func (context *SignContext) fetchRevocationData() error {
|
|||||||
|
|
||||||
// Calculate space needed for signature.
|
// Calculate space needed for signature.
|
||||||
for _, crl := range context.SignData.RevocationData.CRL {
|
for _, crl := range context.SignData.RevocationData.CRL {
|
||||||
context.SignatureMaxLength += uint32(len(crl.FullBytes) * 2)
|
context.SignatureMaxLength += uint32(hex.EncodedLen(len(crl.FullBytes)))
|
||||||
}
|
}
|
||||||
for _, ocsp := range context.SignData.RevocationData.OCSP {
|
for _, ocsp := range context.SignData.RevocationData.OCSP {
|
||||||
context.SignatureMaxLength += uint32(len(ocsp.FullBytes) * 2)
|
context.SignatureMaxLength += uint32(hex.EncodedLen(len(ocsp.FullBytes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (context *SignContext) createSignature() ([]byte, error) {
|
func (context *SignContext) createSignature() ([]byte, error) {
|
||||||
|
context.OutputBuffer.Seek(0, 0)
|
||||||
|
|
||||||
// Sadly we can't efficiently sign a file, we need to read all the bytes we want to sign.
|
// Sadly we can't efficiently sign a file, we need to read all the bytes we want to sign.
|
||||||
context.OutputFile.Seek(0, 0)
|
file_content := context.OutputBuffer.Buff.Bytes()
|
||||||
sign_buf := bytes.NewBuffer(nil)
|
|
||||||
io.Copy(sign_buf, context.OutputFile)
|
|
||||||
file_content := sign_buf.Bytes()
|
|
||||||
|
|
||||||
// Collect the parts to sign.
|
// Collect the parts to sign.
|
||||||
sign_content := make([]byte, 0)
|
sign_content := make([]byte, 0)
|
||||||
@@ -266,7 +271,17 @@ func (context *SignContext) replaceSignature() error {
|
|||||||
return errors.New("Signature is too big to fit in reserved space.")
|
return errors.New("Signature is too big to fit in reserved space.")
|
||||||
}
|
}
|
||||||
|
|
||||||
context.OutputFile.WriteAt(dst, context.ByteRangeValues[0]+context.ByteRangeValues[1]+1)
|
context.OutputBuffer.Seek(0, 0)
|
||||||
|
file_content := context.OutputBuffer.Buff.Bytes()
|
||||||
|
|
||||||
|
context.OutputBuffer.Write(file_content[:(context.ByteRangeValues[0] + context.ByteRangeValues[1] + 1)])
|
||||||
|
|
||||||
|
// Write new ByteRange.
|
||||||
|
if _, err := context.OutputBuffer.Write([]byte(dst)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
context.OutputBuffer.Write(file_content[(context.ByteRangeValues[0]+context.ByteRangeValues[1]+1)+int64(len(dst)):])
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
81
sign/pdfsignature_test.go
Normal file
81
sign/pdfsignature_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bitbucket.org/digitorus/pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateSignature(t *testing.T) {
|
||||||
|
input_file, err := os.Open("../testfiles/testfile20.pdf")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finfo, err := input_file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := finfo.Size()
|
||||||
|
|
||||||
|
rdr, err := pdf.NewReader(input_file, size)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timezone, _ := time.LoadLocation("Europe/Tallinn")
|
||||||
|
now := time.Date(2017, 9, 23, 14, 39, 0, 0, timezone)
|
||||||
|
|
||||||
|
sign_data := SignData{
|
||||||
|
Signature: SignDataSignature{
|
||||||
|
Info: SignDataSignatureInfo{
|
||||||
|
Name: "Jeroen Bobbeldijk",
|
||||||
|
Location: "Rotterdam",
|
||||||
|
Reason: "Test",
|
||||||
|
ContactInfo: "Geen",
|
||||||
|
Date: now,
|
||||||
|
},
|
||||||
|
CertType: 2,
|
||||||
|
Approval: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3
|
||||||
|
|
||||||
|
context := SignContext{
|
||||||
|
Filesize: size + 1,
|
||||||
|
PDFReader: rdr,
|
||||||
|
InputFile: input_file,
|
||||||
|
VisualSignData: VisualSignData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount),
|
||||||
|
},
|
||||||
|
CatalogData: CatalogData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
|
||||||
|
},
|
||||||
|
InfoData: InfoData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
|
||||||
|
},
|
||||||
|
SignData: sign_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_signature := "13 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached /ByteRange[0 ********** ********** **********] /Contents<> /Reference [ << /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >> ] /Name (Jeroen Bobbeldijk) /Location (Rotterdam) /Reason (Test) /ContactInfo (Geen) /M (D:20170923143900+03'00') >>\nendobj\n"
|
||||||
|
|
||||||
|
signature, byte_range_start_byte, signature_contents_start_byte := context.createSignaturePlaceholder()
|
||||||
|
|
||||||
|
if signature != expected_signature {
|
||||||
|
t.Errorf("Signature mismatch, expected %s, but got %s", expected_signature, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if byte_range_start_byte != 78 {
|
||||||
|
t.Errorf("Byte range start mismatch, expected %d, but got %d", 78, byte_range_start_byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
if signature_contents_start_byte != 135 {
|
||||||
|
t.Errorf("Signature contents start byte mismatch, expected %d, but got %d", 135, signature_contents_start_byte)
|
||||||
|
}
|
||||||
|
}
|
@@ -6,44 +6,47 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (context *SignContext) writeTrailer() error {
|
func (context *SignContext) writeTrailer() error {
|
||||||
trailer_length := context.PDFReader.XrefInformation.IncludingTrailerEndPos - context.PDFReader.XrefInformation.EndPos
|
|
||||||
|
|
||||||
// Read the trailer so we can replace the size.
|
if context.PDFReader.XrefInformation.Type == "table" {
|
||||||
context.InputFile.Seek(context.PDFReader.XrefInformation.EndPos+1, 0)
|
trailer_length := context.PDFReader.XrefInformation.IncludingTrailerEndPos - context.PDFReader.XrefInformation.EndPos
|
||||||
trailer_buf := make([]byte, trailer_length)
|
|
||||||
if _, err := context.InputFile.Read(trailer_buf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
root_string := "Root " + context.CatalogData.RootString
|
// Read the trailer so we can replace the size.
|
||||||
new_root := "Root " + strconv.FormatInt(int64(context.CatalogData.ObjectId), 10) + " 0 R"
|
context.InputFile.Seek(context.PDFReader.XrefInformation.EndPos+1, 0)
|
||||||
|
trailer_buf := make([]byte, trailer_length)
|
||||||
|
if _, err := context.InputFile.Read(trailer_buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
size_string := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10)
|
root_string := "Root " + context.CatalogData.RootString
|
||||||
new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+4, 10)
|
new_root := "Root " + strconv.FormatInt(int64(context.CatalogData.ObjectId), 10) + " 0 R"
|
||||||
|
|
||||||
info := context.PDFReader.Trailer().Key("Info")
|
size_string := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10)
|
||||||
infoPtr := info.GetPtr()
|
new_size := "Size " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+4, 10)
|
||||||
|
|
||||||
info_string := "Info " + strconv.Itoa(int(infoPtr.GetID())) + " " + strconv.Itoa(int(infoPtr.GetGen())) + " R"
|
info := context.PDFReader.Trailer().Key("Info")
|
||||||
new_info := "Info " + strconv.FormatInt(int64(context.InfoData.ObjectId), 10) + " 0 R"
|
infoPtr := info.GetPtr()
|
||||||
|
|
||||||
trailer_string := string(trailer_buf)
|
info_string := "Info " + strconv.Itoa(int(infoPtr.GetID())) + " " + strconv.Itoa(int(infoPtr.GetGen())) + " R"
|
||||||
trailer_string = strings.Replace(trailer_string, root_string, new_root, -1)
|
new_info := "Info " + strconv.FormatInt(int64(context.InfoData.ObjectId), 10) + " 0 R"
|
||||||
trailer_string = strings.Replace(trailer_string, size_string, new_size, -1)
|
|
||||||
trailer_string = strings.Replace(trailer_string, info_string, new_info, -1)
|
|
||||||
|
|
||||||
// Write the new trailer.
|
trailer_string := string(trailer_buf)
|
||||||
if _, err := context.OutputFile.Write([]byte(trailer_string)); err != nil {
|
trailer_string = strings.Replace(trailer_string, root_string, new_root, -1)
|
||||||
return err
|
trailer_string = strings.Replace(trailer_string, size_string, new_size, -1)
|
||||||
|
trailer_string = strings.Replace(trailer_string, info_string, new_info, -1)
|
||||||
|
|
||||||
|
// Write the new trailer.
|
||||||
|
if _, err := context.OutputBuffer.Write([]byte(trailer_string)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the new xref start position.
|
// Write the new xref start position.
|
||||||
if _, err := context.OutputFile.Write([]byte(strconv.FormatInt(context.NewXrefStart, 10) + "\n")); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(strconv.FormatInt(context.NewXrefStart, 10) + "\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write PDF ending.
|
// Write PDF ending.
|
||||||
if _, err := context.OutputFile.Write([]byte("%%EOF")); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte("%%EOF")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package sign
|
package sign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,22 +20,20 @@ func (context *SignContext) createVisualSignature() (visual_signature string, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found_pages {
|
|
||||||
return "", errors.New("Didn't find pages in PDF trailer Root.")
|
|
||||||
}
|
|
||||||
|
|
||||||
rootPtr := root.GetPtr()
|
rootPtr := root.GetPtr()
|
||||||
context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R"
|
context.CatalogData.RootString = strconv.Itoa(int(rootPtr.GetID())) + " " + strconv.Itoa(int(rootPtr.GetGen())) + " R"
|
||||||
|
|
||||||
first_page, err := findFirstPage(root.Key("Pages"))
|
if found_pages {
|
||||||
if err != nil {
|
first_page, err := findFirstPage(root.Key("Pages"))
|
||||||
return "", err
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
first_page_ptr := first_page.GetPtr()
|
||||||
|
|
||||||
|
visual_signature += " /P " + strconv.Itoa(int(first_page_ptr.GetID())) + " " + strconv.Itoa(int(first_page_ptr.GetGen())) + " R"
|
||||||
}
|
}
|
||||||
|
|
||||||
first_page_ptr := first_page.GetPtr()
|
|
||||||
|
|
||||||
visual_signature += " /P " + strconv.Itoa(int(first_page_ptr.GetID())) + " " + strconv.Itoa(int(first_page_ptr.GetGen())) + " R"
|
|
||||||
|
|
||||||
visual_signature += " /F 132"
|
visual_signature += " /F 132"
|
||||||
visual_signature += " /FT /Sig"
|
visual_signature += " /FT /Sig"
|
||||||
visual_signature += " /T " + pdfString("Signature")
|
visual_signature += " /T " + pdfString("Signature")
|
||||||
|
77
sign/pdfvisualsignature_test.go
Normal file
77
sign/pdfvisualsignature_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bitbucket.org/digitorus/pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVisualSignature(t *testing.T) {
|
||||||
|
input_file, err := os.Open("../testfiles/testfile20.pdf")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finfo, err := input_file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := finfo.Size()
|
||||||
|
|
||||||
|
rdr, err := pdf.NewReader(input_file, size)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load test PDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timezone, _ := time.LoadLocation("Europe/Tallinn")
|
||||||
|
now := time.Date(2017, 9, 23, 14, 39, 0, 0, timezone)
|
||||||
|
|
||||||
|
sign_data := SignData{
|
||||||
|
Signature: SignDataSignature{
|
||||||
|
Info: SignDataSignatureInfo{
|
||||||
|
Name: "Jeroen Bobbeldijk",
|
||||||
|
Location: "Rotterdam",
|
||||||
|
Reason: "Test",
|
||||||
|
ContactInfo: "Geen",
|
||||||
|
Date: now,
|
||||||
|
},
|
||||||
|
CertType: 2,
|
||||||
|
Approval: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3
|
||||||
|
|
||||||
|
context := SignContext{
|
||||||
|
Filesize: size + 1,
|
||||||
|
PDFReader: rdr,
|
||||||
|
InputFile: input_file,
|
||||||
|
VisualSignData: VisualSignData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount),
|
||||||
|
},
|
||||||
|
CatalogData: CatalogData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
|
||||||
|
},
|
||||||
|
InfoData: InfoData{
|
||||||
|
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
|
||||||
|
},
|
||||||
|
SignData: sign_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_visual_signature := "10 0 obj\n<< /Type /Annot /Subtype /Widget /Rect [0 0 0 0] /P 4 0 R /F 132 /FT /Sig /T (Signature) /Ff 0 /V 13 0 R >>\nendobj\n"
|
||||||
|
|
||||||
|
visual_signature, err := context.createVisualSignature()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if visual_signature != expected_visual_signature {
|
||||||
|
t.Errorf("Visual signature mismatch, expected %s, but got %s", expected_visual_signature, visual_signature)
|
||||||
|
}
|
||||||
|
}
|
159
sign/pdfxref.go
159
sign/pdfxref.go
@@ -1,18 +1,24 @@
|
|||||||
package sign
|
package sign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (context *SignContext) writeXref() error {
|
func (context *SignContext) writeXref() error {
|
||||||
|
|
||||||
// @todo: support stream xref.
|
|
||||||
|
|
||||||
if context.PDFReader.XrefInformation.Type == "table" {
|
if context.PDFReader.XrefInformation.Type == "table" {
|
||||||
if err := context.writeXrefTable(); err != nil {
|
if err := context.writeXrefTable(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if context.PDFReader.XrefInformation.Type == "stream" {
|
||||||
|
if err := context.writeXrefStream(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.New("Unkwn xref type: " + context.PDFReader.XrefInformation.Type)
|
return errors.New("Unkwn xref type: " + context.PDFReader.XrefInformation.Type)
|
||||||
}
|
}
|
||||||
@@ -21,15 +27,16 @@ func (context *SignContext) writeXref() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (context *SignContext) writeXrefTable() error {
|
func (context *SignContext) writeXrefTable() error {
|
||||||
|
// @todo: maybe we need a prev here too.
|
||||||
xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10)
|
xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount, 10)
|
||||||
new_xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+4, 10)
|
new_xref_size := "xref\n0 " + strconv.FormatInt(context.PDFReader.XrefInformation.ItemCount+4, 10)
|
||||||
|
|
||||||
if _, err := context.OutputFile.Write([]byte(new_xref_size)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(new_xref_size)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the old xref table to the output pdf.
|
// Write the old xref table to the output pdf.
|
||||||
if err := writePartFromSourceFileToTargetFile(context.InputFile, context.OutputFile, context.PDFReader.XrefInformation.StartPos+int64(len(xref_size)), context.PDFReader.XrefInformation.Length-int64(len(xref_size))); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +45,7 @@ func (context *SignContext) writeXrefTable() error {
|
|||||||
visual_signature_xref_line := leftPad(visual_signature_object_start_position, "0", 10-len(visual_signature_object_start_position)) + " 00000 n \n"
|
visual_signature_xref_line := leftPad(visual_signature_object_start_position, "0", 10-len(visual_signature_object_start_position)) + " 00000 n \n"
|
||||||
|
|
||||||
// Write the new catalog xref line.
|
// Write the new catalog xref line.
|
||||||
if _, err := context.OutputFile.Write([]byte(visual_signature_xref_line)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(visual_signature_xref_line)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +54,7 @@ func (context *SignContext) writeXrefTable() error {
|
|||||||
catalog_xref_line := leftPad(catalog_object_start_position, "0", 10-len(catalog_object_start_position)) + " 00000 n \n"
|
catalog_xref_line := leftPad(catalog_object_start_position, "0", 10-len(catalog_object_start_position)) + " 00000 n \n"
|
||||||
|
|
||||||
// Write the new catalog xref line.
|
// Write the new catalog xref line.
|
||||||
if _, err := context.OutputFile.Write([]byte(catalog_xref_line)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(catalog_xref_line)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +63,7 @@ func (context *SignContext) writeXrefTable() error {
|
|||||||
info_xref_line := leftPad(info_object_start_position, "0", 10-len(info_object_start_position)) + " 00000 n \n"
|
info_xref_line := leftPad(info_object_start_position, "0", 10-len(info_object_start_position)) + " 00000 n \n"
|
||||||
|
|
||||||
// Write the new signature xref line.
|
// Write the new signature xref line.
|
||||||
if _, err := context.OutputFile.Write([]byte(info_xref_line)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(info_xref_line)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +72,145 @@ func (context *SignContext) writeXrefTable() error {
|
|||||||
signature_xref_line := leftPad(signature_object_start_position, "0", 10-len(signature_object_start_position)) + " 00000 n \n"
|
signature_xref_line := leftPad(signature_object_start_position, "0", 10-len(signature_object_start_position)) + " 00000 n \n"
|
||||||
|
|
||||||
// Write the new signature xref line.
|
// Write the new signature xref line.
|
||||||
if _, err := context.OutputFile.Write([]byte(signature_xref_line)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(signature_xref_line)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (context *SignContext) writeXrefStream() error {
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
predictor := context.PDFReader.Trailer().Key("DecodeParms").Key("Predictor").Int64()
|
||||||
|
|
||||||
|
streamBytes := []byte{}
|
||||||
|
err := errors.New("")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_info := "Info " + strconv.FormatInt(int64(context.InfoData.ObjectId), 10) + " 0 R"
|
||||||
|
new_root := "Root " + strconv.FormatInt(int64(context.CatalogData.ObjectId), 10) + " 0 R"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := context.OutputBuffer.Write([]byte("stream\n")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := context.OutputBuffer.Write(streamBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := context.OutputBuffer.Write([]byte("\nendstream\n")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeXrefStreamLine(b *bytes.Buffer, xreftype byte, offset int, gen byte) {
|
||||||
|
b.WriteByte(xreftype)
|
||||||
|
b.Write(encodeInt(offset))
|
||||||
|
b.WriteByte(gen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeInt(i int) []byte {
|
||||||
|
result := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(result, uint32(i))
|
||||||
|
return result[1:4]
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodePNGSUBBytes(columns int, data []byte) ([]byte, error) {
|
||||||
|
rowCount := len(data) / columns
|
||||||
|
if len(data)%columns != 0 {
|
||||||
|
return nil, errors.New("Invalid row/column length")
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
tmpRowData := make([]byte, columns)
|
||||||
|
for i := 0; i < rowCount; i++ {
|
||||||
|
rowData := data[columns*i : columns*(i+1)]
|
||||||
|
tmpRowData[0] = rowData[0]
|
||||||
|
for j := 1; j < columns; j++ {
|
||||||
|
tmpRowData[j] = byte(int(rowData[j]-rowData[j-1]) % 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteByte(1)
|
||||||
|
buffer.Write(tmpRowData)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = buffer.Bytes()
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := zlib.NewWriter(&b)
|
||||||
|
w.Write(data)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodePNGUPBytes(columns int, data []byte) ([]byte, error) {
|
||||||
|
rowCount := len(data) / columns
|
||||||
|
if len(data)%columns != 0 {
|
||||||
|
return nil, errors.New("Invalid row/column length")
|
||||||
|
}
|
||||||
|
|
||||||
|
prevRowData := make([]byte, columns)
|
||||||
|
|
||||||
|
// Initially all previous data is zero.
|
||||||
|
for i := 0; i < columns; i++ {
|
||||||
|
prevRowData[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
tmpRowData := make([]byte, columns)
|
||||||
|
for i := 0; i < rowCount; i++ {
|
||||||
|
rowData := data[columns*i : columns*(i+1)]
|
||||||
|
for j := 0; j < columns; j++ {
|
||||||
|
tmpRowData[j] = byte(int(rowData[j]-prevRowData[j]) % 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the previous row for prediction.
|
||||||
|
copy(prevRowData, rowData)
|
||||||
|
|
||||||
|
buffer.WriteByte(2)
|
||||||
|
buffer.Write(tmpRowData)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = buffer.Bytes()
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := zlib.NewWriter(&b)
|
||||||
|
w.Write(data)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
95
sign/sign.go
95
sign/sign.go
@@ -3,12 +3,15 @@ package sign
|
|||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bitbucket.org/digitorus/pdf"
|
"bitbucket.org/digitorus/pdf"
|
||||||
"bitbucket.org/digitorus/pdfsign/revocation"
|
"bitbucket.org/digitorus/pdfsign/revocation"
|
||||||
|
"github.com/digitorus/pkcs7"
|
||||||
|
"github.com/mattetti/filebuffer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CatalogData struct {
|
type CatalogData struct {
|
||||||
@@ -62,8 +65,9 @@ type SignDataSignatureInfo struct {
|
|||||||
|
|
||||||
type SignContext struct {
|
type SignContext struct {
|
||||||
Filesize int64
|
Filesize int64
|
||||||
InputFile *os.File
|
InputFile io.ReadSeeker
|
||||||
OutputFile *os.File
|
OutputFile io.Writer
|
||||||
|
OutputBuffer *filebuffer.Buffer
|
||||||
SignData SignData
|
SignData SignData
|
||||||
CatalogData CatalogData
|
CatalogData CatalogData
|
||||||
VisualSignData VisualSignData
|
VisualSignData VisualSignData
|
||||||
@@ -100,14 +104,18 @@ func SignFile(input string, output string, sign_data SignData) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Sign(input_file, output_file, rdr, size, sign_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sign(input io.ReadSeeker, output io.Writer, rdr *pdf.Reader, size int64, sign_data SignData) error {
|
||||||
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3
|
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3
|
||||||
|
|
||||||
// We do size+1 because we insert a newline.
|
// We do size+1 because we insert a newline.
|
||||||
context := SignContext{
|
context := SignContext{
|
||||||
Filesize: size + 1,
|
Filesize: size + 1,
|
||||||
PDFReader: rdr,
|
PDFReader: rdr,
|
||||||
InputFile: input_file,
|
InputFile: input,
|
||||||
OutputFile: output_file,
|
OutputFile: output,
|
||||||
VisualSignData: VisualSignData{
|
VisualSignData: VisualSignData{
|
||||||
ObjectId: uint32(rdr.XrefInformation.ItemCount),
|
ObjectId: uint32(rdr.XrefInformation.ItemCount),
|
||||||
},
|
},
|
||||||
@@ -120,7 +128,7 @@ func SignFile(input string, output string, sign_data SignData) error {
|
|||||||
SignData: sign_data,
|
SignData: sign_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = context.SignPDF()
|
err := context.SignPDF()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -129,28 +137,71 @@ func SignFile(input string, output string, sign_data SignData) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (context *SignContext) SignPDF() error {
|
func (context *SignContext) SignPDF() error {
|
||||||
// Copy old file into new file.
|
context.OutputBuffer = filebuffer.New([]byte{})
|
||||||
if _, err := io.Copy(context.OutputFile, context.InputFile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err := context.OutputFile.Sync()
|
// Copy old file into new file.
|
||||||
if err != nil {
|
if _, err := io.Copy(context.OutputBuffer, context.InputFile); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// File always needs an empty line after %%EOF.
|
// File always needs an empty line after %%EOF.
|
||||||
if _, err := context.OutputFile.Write([]byte("\n")); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte("\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base size for signature.
|
// Base size for signature.
|
||||||
context.SignatureMaxLength = 100000
|
context.SignatureMaxLength = uint32(hex.EncodedLen(512))
|
||||||
|
|
||||||
|
switch string(context.SignData.Certificate.SignatureAlgorithm) {
|
||||||
|
case "SHA1-RSA":
|
||||||
|
case "ECDSA-SHA1":
|
||||||
|
case "DSA-SHA1":
|
||||||
|
context.SignatureMaxLength += uint32(hex.EncodedLen(128))
|
||||||
|
break
|
||||||
|
case "SHA256-RSA":
|
||||||
|
case "ECDSA-SHA256":
|
||||||
|
case "DSA-SHA256":
|
||||||
|
context.SignatureMaxLength += uint32(hex.EncodedLen(256))
|
||||||
|
break
|
||||||
|
case "SHA384-RSA":
|
||||||
|
case "ECDSA-SHA384":
|
||||||
|
context.SignatureMaxLength += uint32(hex.EncodedLen(384))
|
||||||
|
break
|
||||||
|
case "SHA512-RSA":
|
||||||
|
case "ECDSA-SHA512":
|
||||||
|
context.SignatureMaxLength += uint32(hex.EncodedLen(512))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add size for my certificate.
|
||||||
|
degenerated, err := pkcs7.DegenerateCertificate(context.SignData.Certificate.Raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
context.SignatureMaxLength += uint32(hex.EncodedLen(len(degenerated)))
|
||||||
|
|
||||||
|
// 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 err
|
||||||
|
}
|
||||||
|
|
||||||
|
context.SignatureMaxLength += uint32(hex.EncodedLen(len(degenerated)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add estimated size for TSA.
|
// Add estimated size for TSA.
|
||||||
// We can't kow actual size of TSA until after signing.
|
// We can't kow actual size of TSA until after signing.
|
||||||
if context.SignData.TSA.URL != "" {
|
if context.SignData.TSA.URL != "" {
|
||||||
context.SignatureMaxLength += 10000
|
context.SignatureMaxLength += uint32(hex.EncodedLen(4000))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch revocation data before adding signature placeholder.
|
// Fetch revocation data before adding signature placeholder.
|
||||||
@@ -165,7 +216,7 @@ func (context *SignContext) SignPDF() error {
|
|||||||
context.VisualSignData.Length = int64(len(visual_signature))
|
context.VisualSignData.Length = int64(len(visual_signature))
|
||||||
|
|
||||||
// Write the new catalog object.
|
// Write the new catalog object.
|
||||||
if _, err := context.OutputFile.Write([]byte(visual_signature)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(visual_signature)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +228,7 @@ func (context *SignContext) SignPDF() error {
|
|||||||
context.CatalogData.Length = int64(len(catalog))
|
context.CatalogData.Length = int64(len(catalog))
|
||||||
|
|
||||||
// Write the new catalog object.
|
// Write the new catalog object.
|
||||||
if _, err := context.OutputFile.Write([]byte(catalog)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(catalog)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +243,7 @@ func (context *SignContext) SignPDF() error {
|
|||||||
context.InfoData.Length = int64(len(info))
|
context.InfoData.Length = int64(len(info))
|
||||||
|
|
||||||
// Write the new catalog object.
|
// Write the new catalog object.
|
||||||
if _, err := context.OutputFile.Write([]byte(info)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(info)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +257,7 @@ func (context *SignContext) SignPDF() error {
|
|||||||
context.SignatureContentsStartByte = signature_contents_start_byte
|
context.SignatureContentsStartByte = signature_contents_start_byte
|
||||||
|
|
||||||
// Write the new signature object.
|
// Write the new signature object.
|
||||||
if _, err := context.OutputFile.Write([]byte(signature_object)); err != nil {
|
if _, err := context.OutputBuffer.Write([]byte(signature_object)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,10 +280,10 @@ func (context *SignContext) SignPDF() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = context.OutputFile.Sync()
|
context.OutputBuffer.Seek(0, 0)
|
||||||
if err != nil {
|
file_content := context.OutputBuffer.Buff.Bytes()
|
||||||
return err
|
|
||||||
}
|
context.OutputFile.Write(file_content)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
351
sign/sign_test.go
Normal file
351
sign/sign_test.go
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
4146
testdoc/multi.pdf
4146
testdoc/multi.pdf
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
0
testfiles/.gitkeep
Normal file
0
testfiles/.gitkeep
Normal file
BIN
testfiles/testfile12.pdf
Normal file
BIN
testfiles/testfile12.pdf
Normal file
Binary file not shown.
201
testfiles/testfile20.pdf
Normal file
201
testfiles/testfile20.pdf
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
%PDF-2.0
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/Type /Catalog
|
||||||
|
/Metadata 2 0 R
|
||||||
|
/Pages 3 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/Length 2351
|
||||||
|
/Type /Metadata
|
||||||
|
/Subtype /XML
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Insert XMP tool name here.'>
|
||||||
|
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
||||||
|
<rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
|
||||||
|
<pdf:Producer>Datalogics - example producer program name here</pdf:Producer>
|
||||||
|
<pdf:Copyright>Copyright 2017 PDF Association</pdf:Copyright>
|
||||||
|
<pdf:Keywords>PDF 2.0 sample example</pdf:Keywords>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="" xmlns:xap="http://ns.adobe.com/xap/1.0/">
|
||||||
|
<xap:CreateDate>2017-05-24T10:30:11Z</xap:CreateDate>
|
||||||
|
<xap:MetadataDate>2017-07-11T07:55:11Z</xap:MetadataDate>
|
||||||
|
<xap:ModifyDate>2017-07-11T07:55:11Z</xap:ModifyDate>
|
||||||
|
<xap:CreatorTool>Datalogics - example creator tool name here</xap:CreatorTool>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<dc:format>application/pdf</dc:format>
|
||||||
|
<dc:title>
|
||||||
|
<rdf:Alt>
|
||||||
|
<rdf:li xml:lang="x-default">A simple PDF 2.0 example file</rdf:li>
|
||||||
|
</rdf:Alt>
|
||||||
|
</dc:title>
|
||||||
|
<dc:creator>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>Datalogics Incorporated</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</dc:creator>
|
||||||
|
<dc:description>
|
||||||
|
<rdf:Alt>
|
||||||
|
<rdf:li xml:lang="x-default">Demonstration of a simple PDF 2.0 file.</rdf:li>
|
||||||
|
</rdf:Alt>
|
||||||
|
</dc:description>
|
||||||
|
<dc:rights>
|
||||||
|
<rdf:Alt>
|
||||||
|
<rdf:li xml:lang="x-default">Copyright 2017 PDF Association. Licensed to the public under Creative Commons Attribution-ShareAlike 4.0 International license.</rdf:li>
|
||||||
|
</rdf:Alt>
|
||||||
|
</dc:rights>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="" xmlns:xapRights="http://ns.adobe.com/xap/1.0/rights/">
|
||||||
|
<xapRights:Marked>True</xapRights:Marked>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="" xmlns:cc="http://creativecommons.org/ns#">
|
||||||
|
<cc:license rdf:resource="https://creativecommons.org/licenses/sa/4.0/" />
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="" xmlns:xapMM="http://ns.adobe.com/xap/1.0/mm/">
|
||||||
|
<xapMM:DocumentID>uuid:3eef2166-8332-abb4-3d31-77334578873f</xapMM:DocumentID>
|
||||||
|
<xapMM:InstanceID>uuid:991bcce7-ee70-11a3-91aa-77bbe2181fd8</xapMM:InstanceID>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
||||||
|
</x:xmpmeta>
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Type /Pages
|
||||||
|
/Kids [4 0 R]
|
||||||
|
/Count 1
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Type /Page
|
||||||
|
/Parent 3 0 R
|
||||||
|
/MediaBox [0 0 612 396]
|
||||||
|
/Contents [5 0 R 6 0 R]
|
||||||
|
/Resources <<
|
||||||
|
/Font << /F1 7 0 R >>
|
||||||
|
>>
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Length 744 >>
|
||||||
|
stream
|
||||||
|
% Save the current graphic state
|
||||||
|
q
|
||||||
|
|
||||||
|
% Draw a black line segment, using the default line width.
|
||||||
|
150 250 m
|
||||||
|
150 350 l
|
||||||
|
S
|
||||||
|
|
||||||
|
% Draw a thicker, dashed line segment.
|
||||||
|
4 w % Set line width to 4 points
|
||||||
|
[4 6] 0 d % Set dash pattern to 4 units on, 6 units off
|
||||||
|
150 250 m
|
||||||
|
400 250 l
|
||||||
|
S
|
||||||
|
[] 0 d % Reset dash pattern to a solid line
|
||||||
|
1 w % Reset line width to 1 unit
|
||||||
|
|
||||||
|
% Draw a rectangle with a 1-unit red border, filled with light blue.
|
||||||
|
1.0 0.0 0.0 RG % Red for stroke color
|
||||||
|
0.5 0.75 1.0 rg % Light blue for fill color
|
||||||
|
200 300 50 75 re
|
||||||
|
B
|
||||||
|
|
||||||
|
% Draw a curve filled with gray and with a colored border.
|
||||||
|
0.5 0.1 0.2 RG
|
||||||
|
0.7 g
|
||||||
|
300 300 m
|
||||||
|
300 400 400 400 400 300 c
|
||||||
|
b
|
||||||
|
|
||||||
|
% Restore the graphic state to what it was at the beginning of this stream
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Length 166 >>
|
||||||
|
stream
|
||||||
|
% A text block that shows "Hello World"
|
||||||
|
% No color is set, so this defaults to black in DeviceGray colorspace
|
||||||
|
BT
|
||||||
|
/F1 24 Tf
|
||||||
|
100 100 Td
|
||||||
|
(Hello World) Tj
|
||||||
|
ET
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Type /Font
|
||||||
|
/Subtype /Type1
|
||||||
|
/BaseFont /Helvetica
|
||||||
|
/FirstChar 33
|
||||||
|
/LastChar 126
|
||||||
|
/Widths 8 0 R
|
||||||
|
/FontDescriptor 9 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
8 0 obj
|
||||||
|
[ 278 355 556 556 889 667 222 333 333 389 584 278 333 278 278 556
|
||||||
|
556 556 556 556 556 556 556 556 556 278 278 584 584 584 556 1015
|
||||||
|
667 667 722 722 667 611 778 722 278 500 667 556 833 722 778 667
|
||||||
|
778 722 667 611 722 667 944 667 667 611 278 278 278 469 556 222
|
||||||
|
556 556 500 556 556 278 556 556 222 222 500 222 833 556 556 556
|
||||||
|
556 333 500 278 556 500 722 500 500 500 334 260 334 584 ]
|
||||||
|
endobj
|
||||||
|
|
||||||
|
% This FontDescriptor contains only the required entries for PDF 2.0
|
||||||
|
% for unembedded standard 14 fonts that contain Latin characters
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Type /FontDescriptor
|
||||||
|
/FontName /Helvetica
|
||||||
|
/Flags 32
|
||||||
|
/FontBBox [ -166 -225 1000 931 ]
|
||||||
|
/ItalicAngle 0
|
||||||
|
/Ascent 718
|
||||||
|
/Descent -207
|
||||||
|
/CapHeight 718
|
||||||
|
/StemV 88
|
||||||
|
/MissingWidth 0
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
% The object cross-reference table. The first entry
|
||||||
|
% denotes the start of PDF data in this file.
|
||||||
|
xref
|
||||||
|
0 10
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000012 00000 n
|
||||||
|
0000000092 00000 n
|
||||||
|
0000002543 00000 n
|
||||||
|
0000002615 00000 n
|
||||||
|
0000002778 00000 n
|
||||||
|
0000003583 00000 n
|
||||||
|
0000003807 00000 n
|
||||||
|
0000003968 00000 n
|
||||||
|
0000004520 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/Size 10
|
||||||
|
/Root 1 0 R
|
||||||
|
/ID [ <31c7a8a269e4c59bc3cd7df0dabbf388><31c7a8a269e4c59bc3cd7df0dabbf388> ]
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
4847
|
||||||
|
%%EOF
|
@@ -10,13 +10,13 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"crypto"
|
||||||
|
|
||||||
"bitbucket.org/digitorus/pdf"
|
"bitbucket.org/digitorus/pdf"
|
||||||
"bitbucket.org/digitorus/pdfsign/revocation"
|
"bitbucket.org/digitorus/pdfsign/revocation"
|
||||||
"github.com/digitorus/pkcs7"
|
"github.com/digitorus/pkcs7"
|
||||||
"github.com/digitorus/timestamp"
|
"github.com/digitorus/timestamp"
|
||||||
"log"
|
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
"crypto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
@@ -187,10 +187,10 @@ func Verify(file *os.File) (apiResp *Response, err error) {
|
|||||||
signer.ValidSignature = true
|
signer.ValidSignature = true
|
||||||
signer.TrustedIssuer = false
|
signer.TrustedIssuer = false
|
||||||
}
|
}
|
||||||
log.Println("Invalid sig")
|
//log.Println("Invalid sig")
|
||||||
apiResp.Error = fmt.Sprintln("Failed to verify signature:", err)
|
apiResp.Error = fmt.Sprintln("Failed to verify signature:", err)
|
||||||
} else {
|
} else {
|
||||||
log.Println("Valid sig")
|
//log.Println("Valid sig")
|
||||||
signer.ValidSignature = true
|
signer.ValidSignature = true
|
||||||
signer.TrustedIssuer = true
|
signer.TrustedIssuer = true
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user