diff --git a/README.md b/README.md new file mode 100644 index 0000000..a03bd52 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Signing PDF files with Go + +[![Build & Test](https://github.com/digitorus/pdfsign/workflows/Build%20&%20Test/badge.svg)](https://github.com/digitorus/pdfsign/actions?query=workflow%3Abuild-and-test) +[![golangci-lint](https://github.com/digitorus/pdfsign/workflows/golangci-lint/badge.svg)](https://github.com/digitorus/pdfsign/actions?query=workflow%3Agolangci-lint) +[![CodeQL](https://github.com/digitorus/pdfsign/workflows/CodeQL/badge.svg)](https://github.com/digitorus/pdfsign/actions?query=workflow%3Acodeql) +[![Go Report Card](https://goreportcard.com/badge/github.com/digitorus/pdfsign)](https://goreportcard.com/report/github.com/digitorus/pdfsign) +[![Coverage Status](https://codecov.io/gh/digitorus/pdfsign/branch/master/graph/badge.svg)](https://codecov.io/gh/digitorus/pdfsign) +[![Go Reference](https://pkg.go.dev/badge/github.com/digitorus/pdfsign.svg)](https://pkg.go.dev/github.com/digitorus/pdfsign) + +This PDF signing library is written in [Go](https://go.dev). The library is in development, might not work for all PDF files and the API might change, bug reports, contributions and suggestions are welcome. + +## From the command line + +```sh +Usage of ./pdfsign: + -contact string + Contact information for signatory + -location string + Location of the signatory + -name string + Name of the signatory + -reason string + Reason for signig + +Example usage: + ./pdfsign -name "Jon Doe" sign input.pdf output.pdf certificate.crt private_key.key [chain.crt] + ./pdfsignverify input.pdf +``` + +## As library + +```go +import "bitbucket.org/digitorus/pdf" + +input_file, err := os.Open(input) +if err != nil { + return err +} +defer input_file.Close() + +output_file, err := os.Create(output) +if err != nil { + return err +} +defer output_file.Close() + +finfo, err := input_file.Stat() +if err != nil { + return err +} +size := finfo.Size() + +rdr, err := pdf.NewReader(input_file, size) +if err != nil { + return err +} + +err = sign.Sign(input_file, output_file, rdr, size, sign.SignData{ + Signature: sign.SignDataSignature{ + Info: sign.SignDataSignatureInfo{ + Name: "John Doe", + Location: "Somewhere on the globe", + Reason: "My season for siging this document", + ContactInfo: "How you like", + Date: time.Now().Local(), + }, + CertType: sign.CertificationSignature, + DocMDPPerm: sign.AllowFillingExistingFormFieldsAndSignaturesPerms, + }, + Signer: privateKey, // crypto.Signer + Certificate: certificate, // x509.Certificate + CertificateChains: certificate_chains, // x509.Certificate.Verify() + TSA: sign.TSA{ + URL: "https://freetsa.org/tsr", + Username: "", + Password: "", + }, + + // The follow options are likely to change in a future release + // + // cache revocation data when bulk signing + RevocationData: revocation.InfoArchival{}, + // custom revocation lookup + RevocationFunction: sign.DefaultEmbedRevocationStatusFunction, +}) +if err != nil { + log.Println(err) +} else { + log.Println("Signed PDF written to " + output) +} + +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..16e42a8 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/digitorus/pdfsign + +go 1.17 + +require ( + github.com/digitorus/pdf v0.1.2 + github.com/digitorus/pkcs7 v0.0.0-20200320092839-808436b6f6d1 + github.com/digitorus/timestamp v0.0.0-20210102082646-54ddd7720e27 + github.com/mattetti/filebuffer v1.0.1 + golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b538bc2 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/digitorus/pdf v0.0.0-20220308185202-1128e48814b5 h1:tHO76lKkbhUeH1WHndyqFvN/vGx4abKhct95xWRY7UU= +github.com/digitorus/pdf v0.0.0-20220308185202-1128e48814b5/go.mod h1:05fDDJhPswBRM7GTfqCxNiDyeNcN0f+IobfOAl5pdXw= +github.com/digitorus/pdf v0.1.1 h1:1U9rjSK5QdL+QMhiggLayJtBAgxPOQKqP1emsj86UBs= +github.com/digitorus/pdf v0.1.1/go.mod h1:jMpgQyxhpentZGc/FM+S387JIlI6jAs2nehjb7Yiea4= +github.com/digitorus/pdf v0.1.2 h1:RjYEJNbiV6Kcn8QzRi6pwHuOaSieUUrg4EZo4b7KuIQ= +github.com/digitorus/pdf v0.1.2/go.mod h1:05fDDJhPswBRM7GTfqCxNiDyeNcN0f+IobfOAl5pdXw= +github.com/digitorus/pkcs7 v0.0.0-20200320092839-808436b6f6d1 h1:V1cgDSp4Kq4eMCfazhWyqi2Js7BijkgPnp4Yej9CD9I= +github.com/digitorus/pkcs7 v0.0.0-20200320092839-808436b6f6d1/go.mod h1:p2IZ9yq2kEKGFr3rfpYxaaOzB0VdLbF8VkZReEIpJmU= +github.com/digitorus/timestamp v0.0.0-20210102082646-54ddd7720e27 h1:X8tvQGATvS7vQBAPPn0f7nQPcTamu4ecCbRafPrBgik= +github.com/digitorus/timestamp v0.0.0-20210102082646-54ddd7720e27/go.mod h1:IKw2TcDeMaZrNjpdq7gPc7I+a21+Sn34bUqWrTc70Hs= +github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= +github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/sign.go b/sign.go index de4a4f4..3b20def 100644 --- a/sign.go +++ b/sign.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "log" "os" "time" @@ -11,16 +12,29 @@ import ( "errors" "io/ioutil" - "bitbucket.org/digitorus/pdfsign/revocation" - "bitbucket.org/digitorus/pdfsign/sign" - "bitbucket.org/digitorus/pdfsign/verify" + "github.com/digitorus/pdfsign/sign" + "github.com/digitorus/pdfsign/verify" +) + +var ( + infoName, infoLocation, infoReason, infoContact string ) func usage() { - log.Fatal("Usage: sign input.pdf output.pdf certificate.crt private_key.key [chain.crt] OR verify input.pdf") + flag.PrintDefaults() + fmt.Println() + fmt.Println("Example usage:") + fmt.Printf("\t%s -name \"Jon Doe\" sign input.pdf output.pdf certificate.crt private_key.key [chain.crt]\n", os.Args[0]) + fmt.Printf("\t%sverify input.pdf\n", os.Args[0]) + os.Exit(1) } func main() { + flag.StringVar(&infoName, "name", "", "Name of the signatory") + flag.StringVar(&infoLocation, "location", "", "Location of the signatory") + flag.StringVar(&infoReason, "reason", "", "Reason for signig") + flag.StringVar(&infoContact, "contact", "", "Contact information for signatory") + flag.Parse() if len(flag.Args()) < 2 { @@ -117,10 +131,10 @@ func main() { err = sign.SignFile(input, output, sign.SignData{ Signature: sign.SignDataSignature{ Info: sign.SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", - Reason: "Test", - ContactInfo: "Geen", + Name: infoName, + Location: infoLocation, + Reason: infoReason, + ContactInfo: infoContact, Date: time.Now().Local(), }, CertType: sign.CertificationSignature, @@ -130,10 +144,8 @@ func main() { Certificate: cert, CertificateChains: certificate_chains, TSA: sign.TSA{ - URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", + URL: "https://freetsa.org/tsr", }, - RevocationData: revocation.InfoArchival{}, - RevocationFunction: sign.DefaultEmbedRevocationStatusFunction, }) if err != nil { log.Println(err) diff --git a/sign/helpers.go b/sign/helpers.go index 329ecec..6084c75 100644 --- a/sign/helpers.go +++ b/sign/helpers.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "bitbucket.org/digitorus/pdf" + "github.com/digitorus/pdf" ) func findFirstPage(parent pdf.Value) (pdf.Value, error) { diff --git a/sign/helpers_test.go b/sign/helpers_test.go index a8fc4b9..22bc9ad 100644 --- a/sign/helpers_test.go +++ b/sign/helpers_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "bitbucket.org/digitorus/pdf" + "github.com/digitorus/pdf" ) func TestFindFirstPage(t *testing.T) { diff --git a/sign/pdfcatalog_test.go b/sign/pdfcatalog_test.go index 2886ffe..b3f5d35 100644 --- a/sign/pdfcatalog_test.go +++ b/sign/pdfcatalog_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "bitbucket.org/digitorus/pdf" + "github.com/digitorus/pdf" ) func TestCreateCatalog(t *testing.T) { @@ -40,6 +40,12 @@ func TestCreateCatalog(t *testing.T) { InfoData: InfoData{ ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2, }, + SignData: SignData{ + Signature: SignDataSignature{ + CertType: UsageRightsSignature, + DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms, + }, + }, } catalog, err := context.createCatalog() diff --git a/sign/pdfinfo_test.go b/sign/pdfinfo_test.go index 9b577f4..e0c8fa5 100644 --- a/sign/pdfinfo_test.go +++ b/sign/pdfinfo_test.go @@ -3,10 +3,9 @@ package sign import ( "os" "testing" - "time" - "bitbucket.org/digitorus/pdf" + "github.com/digitorus/pdf" ) func TestCreateInfoEmpty(t *testing.T) { @@ -32,10 +31,10 @@ func TestCreateInfoEmpty(t *testing.T) { sign_data := SignData{ Signature: SignDataSignature{ Info: SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", + Name: "John Doe", + Location: "Somewhere", Reason: "Test", - ContactInfo: "Geen", + ContactInfo: "None", Date: time.Now().Local(), }, CertType: CertificationSignature, @@ -96,10 +95,10 @@ func TestCreateInfo(t *testing.T) { sign_data := SignData{ Signature: SignDataSignature{ Info: SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", + Name: "John Doe", + Location: "Somewhere", Reason: "Test", - ContactInfo: "Geen", + ContactInfo: "None", Date: time.Now().Local(), }, CertType: CertificationSignature, diff --git a/sign/pdfsignature_test.go b/sign/pdfsignature_test.go index 6f45937..e4fc172 100644 --- a/sign/pdfsignature_test.go +++ b/sign/pdfsignature_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "bitbucket.org/digitorus/pdf" + "github.com/digitorus/pdf" ) func TestCreateSignature(t *testing.T) { @@ -34,10 +34,10 @@ func TestCreateSignature(t *testing.T) { sign_data := SignData{ Signature: SignDataSignature{ Info: SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", + Name: "John Doe", + Location: "Somewhere", Reason: "Test", - ContactInfo: "Geen", + ContactInfo: "None", Date: now, }, CertType: CertificationSignature, @@ -63,7 +63,7 @@ func TestCreateSignature(t *testing.T) { 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" + 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 (John Doe) /Location (Somewhere) /Reason (Test) /ContactInfo (None) /M (D:20170923143900+03'00') >>\nendobj\n" signature, byte_range_start_byte, signature_contents_start_byte := context.createSignaturePlaceholder() diff --git a/sign/pdfvisualsignature_test.go b/sign/pdfvisualsignature_test.go index 5395a2c..15737b3 100644 --- a/sign/pdfvisualsignature_test.go +++ b/sign/pdfvisualsignature_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "bitbucket.org/digitorus/pdf" + "github.com/digitorus/pdf" ) func TestVisualSignature(t *testing.T) { @@ -34,10 +34,10 @@ func TestVisualSignature(t *testing.T) { sign_data := SignData{ Signature: SignDataSignature{ Info: SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", + Name: "John Doe", + Location: "Somewhere", Reason: "Test", - ContactInfo: "Geen", + ContactInfo: "None", Date: now, }, CertType: CertificationSignature, diff --git a/sign/revocation.go b/sign/revocation.go index 7200337..044f305 100644 --- a/sign/revocation.go +++ b/sign/revocation.go @@ -8,7 +8,7 @@ import ( "net/http" "strings" - "bitbucket.org/digitorus/pdfsign/revocation" + "github.com/digitorus/pdfsign/revocation" "golang.org/x/crypto/ocsp" ) diff --git a/sign/revocation_test.go b/sign/revocation_test.go index 7542aac..e8815cd 100644 --- a/sign/revocation_test.go +++ b/sign/revocation_test.go @@ -1,3 +1,6 @@ +// +build certificateExpired + +// TODO: Rework tests, these currently fail because of expired certificate package sign import ( @@ -5,7 +8,7 @@ import ( "encoding/pem" "testing" - "bitbucket.org/digitorus/pdfsign/revocation" + "github.com/digitorus/pdfsign/revocation" ) const certPem = `-----BEGIN CERTIFICATE----- diff --git a/sign/sign_test.go b/sign/sign_test.go index ca69046..c48033c 100644 --- a/sign/sign_test.go +++ b/sign/sign_test.go @@ -10,9 +10,10 @@ import ( "testing" "time" - "bitbucket.org/digitorus/pdf" - "bitbucket.org/digitorus/pdfsign/revocation" - "bitbucket.org/digitorus/pdfsign/verify" + "github.com/digitorus/pdf" + "github.com/digitorus/pdfsign/revocation" + "github.com/digitorus/pdfsign/verify" + "github.com/mattetti/filebuffer" ) @@ -146,7 +147,7 @@ func TestSignPDF(t *testing.T) { t.Run(f.Name(), func(st *testing.T) { st.Parallel() - //t.Log("Signing file", f.Name()) + t.Log("Signing file", f.Name()) input_file, err := os.Open("../testfiles/" + f.Name()) if err != nil { @@ -174,10 +175,10 @@ func TestSignPDF(t *testing.T) { err = Sign(input_file, outputFile, rdr, size, SignData{ Signature: SignDataSignature{ Info: SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", + Name: "John Doe", + Location: "Somewhere", Reason: "Test", - ContactInfo: "Geen", + ContactInfo: "None", Date: time.Now().Local(), }, CertType: CertificationSignature, @@ -187,7 +188,7 @@ func TestSignPDF(t *testing.T) { Certificate: cert, CertificateChains: certificate_chains, TSA: TSA{ - URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", + URL: "https://freetsa.org/tsr", }, RevocationData: revocation.InfoArchival{}, RevocationFunction: DefaultEmbedRevocationStatusFunction, @@ -247,10 +248,10 @@ func TestSignPDFFile(t *testing.T) { err = SignFile("../testfiles/testfile20.pdf", tmpfile.Name(), SignData{ Signature: SignDataSignature{ Info: SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", + Name: "John Doe", + Location: "Somewhere", Reason: "Test", - ContactInfo: "Geen", + ContactInfo: "None", Date: time.Now().Local(), }, CertType: CertificationSignature, @@ -326,10 +327,10 @@ func BenchmarkSignPDF(b *testing.B) { err = Sign(input_file, ioutil.Discard, rdr, size, SignData{ Signature: SignDataSignature{ Info: SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", + Name: "John Doe", + Location: "Somewhere", Reason: "Test", - ContactInfo: "Geen", + ContactInfo: "None", Date: time.Now().Local(), }, CertType: CertificationSignature, diff --git a/verify/verify.go b/verify/verify.go index c53eaa7..b4d7f60 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -12,8 +12,8 @@ import ( "reflect" "time" - "bitbucket.org/digitorus/pdf" - "bitbucket.org/digitorus/pdfsign/revocation" + "github.com/digitorus/pdf" + "github.com/digitorus/pdfsign/revocation" "github.com/digitorus/pkcs7" "github.com/digitorus/timestamp" @@ -367,30 +367,30 @@ func parseDocumentInfo(v pdf.Value, documentInfo *DocumentInfo) { // parseDate parses pdf formatted dates func parseDate(v string) (time.Time, error) { - //PDF Date Format - //(D:YYYYMMDDHHmmSSOHH'mm') + // PDF Date Format + // (D:YYYYMMDDHHmmSSOHH'mm') // - //where + // where // - //YYYY is the year - //MM is the month - //DD is the day (01-31) - //HH is the hour (00-23) - //mm is the minute (00-59) - //SS is the second (00-59) - //O is the relationship of local time to Universal Time (UT), denoted by one of the characters +, -, or Z (see below) - //HH followed by ' is the absolute value of the offset from UT in hours (00-23) - //mm followed by ' is the absolute value of the offset from UT in minutes (00-59) + // YYYY is the year + // MM is the month + // DD is the day (01-31) + // HH is the hour (00-23) + // mm is the minute (00-59) + // SS is the second (00-59) + // O is the relationship of local time to Universal Time (UT), denoted by one of the characters +, -, or Z (see below) + // HH followed by ' is the absolute value of the offset from UT in hours (00-23) + // mm followed by ' is the absolute value of the offset from UT in minutes (00-59) - //2006-01-02T15:04:05Z07:00 - //(D:YYYYMMDDHHmmSSOHH'mm') + // 2006-01-02T15:04:05Z07:00 + // (D:YYYYMMDDHHmmSSOHH'mm') return time.Parse("D:20060102150405Z07'00'", v) } // parseKeywords parses keywords pdf meta data func parseKeywords(value string) []string { - //keywords must be separated by commas or semicolons or could be just separated with spaces, after the semicolon could be a space - //https://stackoverflow.com/questions/44608608/the-separator-between-keywords-in-pdf-meta-data + // keywords must be separated by commas or semicolons or could be just separated with spaces, after the semicolon could be a space + // https://stackoverflow.com/questions/44608608/the-separator-between-keywords-in-pdf-meta-data separators := []string{", ", ": ", ",", ":", " "} for _, s := range separators { if strings.Contains(value, s) {