diff --git a/signatures/README.md b/signatures/README.md index 7cc47c07..7ea367e9 100644 --- a/signatures/README.md +++ b/signatures/README.md @@ -9,6 +9,8 @@ Examples for digital signing of PDF files with UniDoc: - [pdf_sign_validate.go](pdf_sign_validate.go) Example of signature validation. - [pdf_sign_pem_multicert.go](pdf_sign_pem_multicert.go) Example of signing using a certificate chain and a private key, extracted from PEM files. +For LTV enabling digital signatures, see the [LTV](ltv) guide and samples. + ## pkcs_sign_hsm_pkcs11.go The code example shows how to sign with a HSM via PKCS11 as supported by the @@ -81,4 +83,4 @@ Sign PDF file: $ go run pdf_sign_hsm_pkcs11.go sign test input.pdf input_signed.pdf ``` -Signed output is in `input_signed.pdf` +Signed output is in `input_signed.pdf`. diff --git a/signatures/ltv/README.md b/signatures/ltv/README.md new file mode 100644 index 00000000..5f408c7f --- /dev/null +++ b/signatures/ltv/README.md @@ -0,0 +1,383 @@ +# LTV + +## Table of contents + +- [Overview](#overview) +- [LTV enable workflows](#ltv-enable-workflows) + - [Sign and LTV enable in the same revision](#1-sign-and-ltv-enable-in-the-same-revision) + - [Sign and LTV enable in a separate revision](#2-sign-and-ltv-enable-in-a-separate-revision) +- [Usage](#usage) + - [LTV client](#ltv-client) + - [Sign and LTV enable in one revision](#workflow-1-sign-and-ltv-enable-in-one-revision) + - [LTV enable signed file](#workflow-2-ltv-enable-signed-file) + - [Protect validation data by adding a timestamp signature](#protect-validation-data-by-adding-a-timestamp-signature) + - [Customize LTV client](#customize-ltv-client) + +## Overview + +LTV (Long-Term Validation) can be enabled by adding validation information +for the digital signatures applied to a PDF file. + +The validation data required to LTV enable a signature consists of: +- The signing certificate chain, which contains the certificate used to apply + the signature and all its issuers, up to a trusted root (CA) certificate. +- OCSP (Online Certificate Status Protocol) responses for each certificate in + the added certificate chain. +- CRL (Certificate Revocation List) responses for each certificate in the + added certificate chain. + +`NOTE`: a signature can be LTV enabled by including only OCSP or only CRL +responses for the signing certificate chain. However, the more validation +data is included, the better. + +OCSP and CRL responses provide revocation information for the certificates used +in the signing process. They are obtained by querying OCSP/CRL servers. +The responders (servers) used to obtain the OCSP responses are extracted from +the certificates in the signing chain +([x509.Certificate.OCSPServer](https://godoc.org/crypto/x509#Certificate)). +Similarly, the servers used to obtain CRL responses are also extracted from +each certificate in the signing chain +([x509.Certificate.CRLDistributionPoints](https://godoc.org/crypto/x509#Certificate)). + +LTV enabling is done through the use of a DSS (Document Security Store) dictionary, +which contains the validation information described above. + +``` +DSS + // Global validation data. + // The validation data in these fields can be used to validate all + // signatures in a PDF document. + Certs [PDF stream array] + OCSPs [PDF stream array] + CLRs [PDF stream array] + + // Signature specific validation data. + // Each key in the VRI dictionary represents the hash of the Contents + // field of a signature dictionary. Each signature entry in the VRI + // dictionary has its own set of certificates, OCSP and CRL responses, + // used to validate that particular signature exclusively. + VRI: + Hash signature 1: Cert [PDF stream array] + OCSP [PDF stream array] + CLR [PDF stream array] + ... + Hash signature N: Cert [PDF stream array] + OCSP [PDF stream array] + CLR [PDF stream array] +``` + +The Contents field of a signature is only known after signing and writing a +PDF file. In consequence, VRI signature entries cannot be added in the same +revision a signature is applied. However, global validation data can be added +in the signing revision, provided that at least the signing certificate is +known at signing time, which is not always the case (e.g.: when using external +services to apply the signature). + +`NOTE`: Adobe's definition of LTV is not clearly defined. Also, they +have not disclosed their validation process process which results in the +`Signature is LTV enabled` label being displayed for a signature. However, +the general consensus is that both a signing certificate chain (which builds +up to a trusted root certificate) and revocation data for it (OCSP and CRL +responses) need to be included. + +## LTV enable workflows + +#### 1. Sign and LTV enable in the same revision + +Useful when signing and LTV enabling must be done in one revision. At least +the signing certificate must be provided. The validation data is added to the +global scope of the DSS and can be used to validate any of the signatures in +the file. +``` + Revision 1 (signer 1): + - Add validation information (certificates, OCSP and CRL information) to + the global validation data of the DSS dictionary. + - Apply signature 1. + - Write signed file. + ... + Revision N (signer N): + - Add validation information (certificates, OCSP and CRL information) to + the global validation data of the DSS dictionary. + - Apply signature N. + - Write signed file. +``` + +#### 2. Sign and LTV enable in a separate revision +Useful when the validation data is not available at signing time. Furthermore, +both global and signature specific data can be added for all signatures applied +previous revisions. +``` + // Signing revisions. + Revision 1 (signer 1): + - Apply signature 1. + - Write signed file. + ... + Revision N (signer N): + - Apply signature N. + - Write signed file. + + // LTV enable revision. + Revision N+1: + - Add both global and signature specific validation data (certificates, + OCSP and CRL information) to the DSS dictionary. + - Write file. +``` + +The workflows can be repeated as many times as needed, by adding extra +revisions to the document, either for additional signatures or for adding +validation data for the previous revision/revisions. + +`NOTE`: Hybrid workflows (consisting of combinations of the two workflows above) +are also possible. + +## Usage + +#### LTV client + +unipdf provides a configurable client for LTV enabling signed PDF documents, +which supports both workflows described above. + +```go +type LTV + // + // Fields. + // + + CertClient *sigutil.CertClient // Used to retrieve certificates. + OCSPClient *sigutil.OCSPClient // Used to retrieve OCSP information. + CRLClient *sigutil.CRLClient // Used to retrieve CRL information. + + // Specifies whether existing signature validations should be skipped. + SkipExisting bool + + // + // Workflow #1 methods: adding LTV data in the signing revision. + // + + // EnableChain adds the specified certificate chain and validation data + // for it to the global scope of the document DSS. + EnableChain(chain []*x509.Certificate) error + + // + // Workflow #2 methods: adding LTV data in a separate revision. + // + + // LTV enables all signatures in a PDF document. + EnableAll(extraCerts []*x509.Certificate) error + + // Enable LTV enables the specified signature. + Enable(sig *model.PdfSignature, extraCerts []*x509.Certificate) error + + // LTV enables the signature dictionary of the PDF AcroForm field + // identified the specified name. + EnableByName(name string, extraCerts []*x509.Certificate) error +``` + +If `LTV.SkipExisting` is true (the default), existing signature validations +won't be reconstructed when calling `EnableAll`, `Enable` or `EnableByName`, +thus making the LTV process incremental. + +Example: +``` + Revision 1: apply signature 1. + Revision 2: LTV enable by using LTV.EnableAll (will enable signature 1). + Revision 3: apply signature 2. + Revision 4: apply signature 3. + Revision 5: LTV enable by using LTV.EnableAll (will enable signatures 2 and 3). +``` + +#### Workflow #1: sign and LTV enable in one revision + +The sample showcases applying a digital signature to a PDF document, and +LTV enabling the signing certificate chain, in a single revision. The example +uses a `adbe.pkcs7.detached` signature handler. However, the handler can be +swapped with a `adbe.x509.rsa_sha1` handler (no other changes are required). + +The signing certificate chain must be provided (which needs to contain at +least the signing certificate). The LTV client builds the certificate chain +up to a trusted root certificate (by downloading any missing certificates). + +For more information, see [pdf_sign_ltv_one_revision.go](pdf_sign_ltv_one_revision.go). + +```go +// Get private key and X509 certificate from a PKCS12 (.p12/.pfx) file. +pfxData, err := ioutil.ReadFile("p12.pfx") +if err != nil { + log.Fatal(err) +} + +priv, cert, err := pkcs12.Decode(pfxData, password) +if err != nil { + log.Fatal(err) +} + +file, err := os.Open("unsigned-file.pdf") +if err != nil { + log.Fatal(err) +} +defer file.Close() + +// Create appender. +appender, err := model.NewPdfAppender(reader) +if err != nil { + log.Fatal(err) +} + +// Create signature handler. +handler, err := sighandler.NewAdobePKCS7Detached(priv.(*rsa.PrivateKey), cert) +if err != nil { + log.Fatal(err) +} + +// Create signature. +signature := model.NewPdfSignature(handler) +signature.SetName("Test Sign LTV enable") + +if err := signature.Initialize(); err != nil { + log.Fatal(err) +} + +// Create signature appearance. +opts := annotator.NewSignatureFieldOpts() +opts.Rect = []float64{10, 25, 75, 60} + +field, err := annotator.NewSignatureField( + signature, + []*annotator.SignatureLine{ + annotator.NewSignatureLine("Name", "John Doe"), + annotator.NewSignatureLine("Reason", "Signature test"), + }, + opts, +) +field.T = core.MakeString("Test Sign LTV enable") + +if err = appender.Sign(1, field); err != nil { + log.Fatal(err) +} + +// LTV enable the certificate chain used to apply the signature. +ltv, err := model.NewLTV(appender) +if err != nil { + log.Fatal(err) +} +if err := ltv.EnableChain([]*x509.Certificate{cert}); err != nil { + log.Fatal(err) +} + +// Write output file. +if err = appender.WriteToFile("output.pdf"); err != nil { + log.Fatal(err) +} +``` + +#### Workflow #2: LTV enable signed file + +The sample showcases LTV enabling a signed PDF file through an additional +revision. This workflow is useful when the signing certificate is not +available at the time of signing (e.g.: when signing using external services). + +The certificate chains used for the signatures in the file are +extracted from the signature dictionaries. The LTV client builds the +certificate chains up to a trusted root certificate, by downloading any +missing certificates. + +For more information, see [pdf_ltv_enable_signed_file.go](pdf_ltv_enable_signed_file.go) +and [pdf_sign_ltv_extra_revision.go](pdf_sign_ltv_extra_revision.go). + +```go +// Create reader. +file, err := os.Open("signed-file.pdf") +if err != nil { + log.Fatal(err) +} +defer file.Close() + +reader, err := model.NewPdfReader(file) +if err != nil { + log.Fatal(err) +} + +// Create appender. +appender, err := model.NewPdfAppender(reader) +if err != nil { + log.Fatal(err) +} + +// LTV enable. +ltv, err := model.NewLTV(appender) +if err != nil { + log.Fatal(err) +} + +if err := ltv.EnableAll(nil); err != nil { + log.Fatal(err) +} + +// Write output file. +if err = appender.WriteToFile("output.pdf"); err != nil { + log.Fatal(err) +} +``` + +#### Protect validation data by adding a timestamp signature. + +Optionally, an extra revision consisting of a timestamp signature, in +order to protect the added validation information, can be added. + +A timestamp signature to protect the document DSS can be added in both workflows: + +Workflow #1 +``` + Revision 1: apply signature and LTV enable the signing chain. + Revision 2: apply timestamp signature. +``` + +Workflow #2: +``` + Revision 1: apply signature. + Revision 2: LTV enable the added signature and apply a timestamp signature. +``` + +`NOTE`: the timestamp signature itself is not LTV enabled. In order to LTV +enable the timestamp signature, a new revision has to be added (in both +workflows), in order to add validation data for the applied timestamp +signature. + +For more information, see [pdf_sign_ltv_timestamp_revision](pdf_sign_ltv_timestamp_revision.go). + +#### Customize LTV client + +The HTTP clients used by `LTV.CertClient`, `LTV.OCSPClient` and `LTV.CRLClient` +can be customized. + +```go +ltv, err := model.NewLTV(appender) +if err != nil { + log.Fatal(err) +} + +// Set timeout for HTTP requests. +ltv.CertClient.HTTPClient.Timeout = 300 * time.Millisecond + +// Set custom HTTP client. +ltv.OCSPClient.HTTPClient = &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + ForceAttemptHTTP2: true, + }, +} + +// Set HTTP client proxy. +proxyURL, err := url.Parse("https://proxy-addr:proxy-port") +if err != nil { + log.Fatal(err) +} + +ltv.CRLClient.HTTPClient = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + }, + Timeout: 5 * time.Second, +} +``` diff --git a/signatures/ltv/pdf_ltv_enable_signed_file.go b/signatures/ltv/pdf_ltv_enable_signed_file.go new file mode 100644 index 00000000..0a9d5e0a --- /dev/null +++ b/signatures/ltv/pdf_ltv_enable_signed_file.go @@ -0,0 +1,107 @@ +/* + * This example showcases how to LTV enable the signatures in a signed PDF file, + * by adding a second revision to the document, containing the validation data. + * + * $ ./pdf_ltv_enable_signed_file [] + */ + +package main + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "os" + + "github.com/unidoc/unipdf/v3/common/license" + "github.com/unidoc/unipdf/v3/model" +) + +const licenseKey = ` +-----BEGIN UNIDOC LICENSE KEY----- +Free trial license keys are available at: https://unidoc.io/ +-----END UNIDOC LICENSE KEY----- +` + +const usage = "Usage: %s INPUT_PDF_PATH OUTPUT_PDF_PATH [EXTRA_CERTS]\n" + +func init() { + // Enable debug-level logging. + // unicommon.SetLogger(unicommon.NewConsoleLogger(unicommon.LogLevelDebug)) + + err := license.SetLicenseKey(licenseKey, `Company Name`) + if err != nil { + panic(err) + } +} + +func main() { + args := os.Args + if len(args) < 3 { + fmt.Printf(usage, os.Args[0]) + return + } + inputPath := args[1] + outputPath := args[2] + + // Load certificate chain. + var certChain []*x509.Certificate + if len(args) == 4 { + issuerCertData, err := ioutil.ReadFile(args[3]) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + for len(issuerCertData) != 0 { + var block *pem.Block + block, issuerCertData = pem.Decode(issuerCertData) + if block == nil { + break + } + + issuer, err := x509.ParseCertificate(block.Bytes) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + certChain = append(certChain, issuer) + } + } + + // Create reader. + file, err := os.Open(inputPath) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + defer file.Close() + + reader, err := model.NewPdfReader(file) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Create appender. + appender, err := model.NewPdfAppender(reader) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // LTV enable the signed file. + ltv, err := model.NewLTV(appender) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + if err := ltv.EnableAll(certChain); err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Write output PDF file. + err = appender.WriteToFile(outputPath) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + log.Printf("PDF file successfully LTV enabled. Output path: %s\n", outputPath) +} diff --git a/signatures/ltv/pdf_sign_ltv_extra_revision.go b/signatures/ltv/pdf_sign_ltv_extra_revision.go new file mode 100644 index 00000000..4a0fddf8 --- /dev/null +++ b/signatures/ltv/pdf_sign_ltv_extra_revision.go @@ -0,0 +1,203 @@ +/* + * This example showcases how to digitally sign a PDF file using a + * PKCS12 (.p12/.pfx) file and LTV enable the signature by adding a second + * revision to the document, containing the validation data. + * + * $ ./pdf_sign_ltv_extra_revision [] + */ + +package main + +import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "time" + + "golang.org/x/crypto/pkcs12" + + "github.com/unidoc/unipdf/v3/annotator" + "github.com/unidoc/unipdf/v3/common/license" + "github.com/unidoc/unipdf/v3/core" + "github.com/unidoc/unipdf/v3/model" + "github.com/unidoc/unipdf/v3/model/sighandler" +) + +const licenseKey = ` +-----BEGIN UNIDOC LICENSE KEY----- +Free trial license keys are available at: https://unidoc.io/ +-----END UNIDOC LICENSE KEY----- +` + +const usage = "Usage: %s P12_FILE PASSWORD INPUT_PDF_PATH OUTPUT_PDF_PATH [EXTRA_CERTS]\n" + +func init() { + // Enable debug-level logging. + // unicommon.SetLogger(unicommon.NewConsoleLogger(unicommon.LogLevelDebug)) + + err := license.SetLicenseKey(licenseKey, `Company Name`) + if err != nil { + panic(err) + } +} + +func main() { + args := os.Args + if len(args) < 5 { + fmt.Printf(usage, os.Args[0]) + return + } + p12Path := args[1] + password := args[2] + inputPath := args[3] + outputPath := args[4] + + // Load private key and X509 certificate from the PKCS12 file. + pfxData, err := ioutil.ReadFile(p12Path) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + priv, cert, err := pkcs12.Decode(pfxData, password) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Load certificate chain. + certChain := []*x509.Certificate{cert} + if len(args) == 6 { + issuerCertData, err := ioutil.ReadFile(args[5]) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + for len(issuerCertData) != 0 { + var block *pem.Block + block, issuerCertData = pem.Decode(issuerCertData) + if block == nil { + break + } + + issuer, err := x509.ParseCertificate(block.Bytes) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + certChain = append(certChain, issuer) + } + } + + // Sign and write file to buffer. + signedBytes, err := signFile(inputPath, priv.(*rsa.PrivateKey), cert) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // LTV enable and write file to disk. + err = ltvEnable(bytes.NewReader(signedBytes), outputPath, certChain) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + log.Printf("PDF file successfully signed. Output path: %s\n", outputPath) +} + +func signFile(inputPath string, priv *rsa.PrivateKey, cert *x509.Certificate) ([]byte, error) { + // Create reader. + file, err := os.Open(inputPath) + if err != nil { + return nil, err + } + defer file.Close() + + reader, err := model.NewPdfReader(file) + if err != nil { + return nil, err + } + + // Create appender. + appender, err := model.NewPdfAppender(reader) + if err != nil { + return nil, err + } + + // Create signature handler. + handler, err := sighandler.NewAdobePKCS7Detached(priv, cert) + if err != nil { + return nil, err + } + + // Create signature. + signature := model.NewPdfSignature(handler) + signature.SetName("Test Sign LTV enable") + signature.SetReason("TestSignLTV") + signature.SetDate(time.Now(), "") + + if err := signature.Initialize(); err != nil { + return nil, err + } + + // Create signature field and appearance. + opts := annotator.NewSignatureFieldOpts() + opts.FontSize = 10 + opts.Rect = []float64{10, 25, 75, 60} + + field, err := annotator.NewSignatureField( + signature, + []*annotator.SignatureLine{ + annotator.NewSignatureLine("Name", "John Doe"), + annotator.NewSignatureLine("Date", "2019.16.04"), + annotator.NewSignatureLine("Reason", "Signature test"), + }, + opts, + ) + field.T = core.MakeString("Test Sign LTV enable") + + if err = appender.Sign(1, field); err != nil { + return nil, err + } + + // Write output PDF file to buffer. + buf := bytes.NewBuffer(nil) + if err = appender.Write(buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func ltvEnable(r io.ReadSeeker, outputPath string, certChain []*x509.Certificate) error { + // Create reader. + reader, err := model.NewPdfReader(r) + if err != nil { + return err + } + + // Create appender. + appender, err := model.NewPdfAppender(reader) + if err != nil { + return err + } + + // LTV enable the certificate chain used to apply the signature. + ltv, err := model.NewLTV(appender) + if err != nil { + return err + } + + if err := ltv.EnableAll(certChain); err != nil { + return err + } + + // Write output PDF file. + if err = appender.WriteToFile(outputPath); err != nil { + return err + } + + return nil +} diff --git a/signatures/ltv/pdf_sign_ltv_one_revision.go b/signatures/ltv/pdf_sign_ltv_one_revision.go new file mode 100644 index 00000000..3ca61489 --- /dev/null +++ b/signatures/ltv/pdf_sign_ltv_one_revision.go @@ -0,0 +1,162 @@ +/* + * This example showcases how to digitally sign a PDF file using a + * PKCS12 (.p12/.pfx) file and LTV enable the signature in one PDF revision. + * + * $ ./pdf_sign_ltv_one_revision [] + */ + +package main + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "os" + "time" + + "golang.org/x/crypto/pkcs12" + + "github.com/unidoc/unipdf/v3/annotator" + "github.com/unidoc/unipdf/v3/common/license" + "github.com/unidoc/unipdf/v3/core" + "github.com/unidoc/unipdf/v3/model" + "github.com/unidoc/unipdf/v3/model/sighandler" +) + +const licenseKey = ` +-----BEGIN UNIDOC LICENSE KEY----- +Free trial license keys are available at: https://unidoc.io/ +-----END UNIDOC LICENSE KEY----- +` + +const usage = "Usage: %s P12_FILE PASSWORD INPUT_PDF_PATH OUTPUT_PDF_PATH [EXTRA_CERTS]\n" + +func init() { + // Enable debug-level logging. + // unicommon.SetLogger(unicommon.NewConsoleLogger(unicommon.LogLevelDebug)) + + err := license.SetLicenseKey(licenseKey, `Company Name`) + if err != nil { + panic(err) + } +} + +func main() { + args := os.Args + if len(args) < 5 { + fmt.Printf(usage, os.Args[0]) + return + } + p12Path := args[1] + password := args[2] + inputPath := args[3] + outputPath := args[4] + + // Get private key and X509 certificate from the PKCS12 file. + pfxData, err := ioutil.ReadFile(p12Path) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + priv, cert, err := pkcs12.Decode(pfxData, password) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Load certificate chain. + certChain := []*x509.Certificate{cert} + if len(args) == 6 { + issuerCertData, err := ioutil.ReadFile(args[5]) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + for len(issuerCertData) != 0 { + var block *pem.Block + block, issuerCertData = pem.Decode(issuerCertData) + if block == nil { + break + } + + issuer, err := x509.ParseCertificate(block.Bytes) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + certChain = append(certChain, issuer) + } + } + + // Create reader. + file, err := os.Open(inputPath) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + defer file.Close() + + reader, err := model.NewPdfReader(file) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Create appender. + appender, err := model.NewPdfAppender(reader) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Create signature handler. + handler, err := sighandler.NewAdobePKCS7Detached(priv.(*rsa.PrivateKey), cert) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Create signature. + signature := model.NewPdfSignature(handler) + signature.SetName("Test Sign LTV enable") + signature.SetReason("TestSignLTV") + signature.SetDate(time.Now(), "") + + if err := signature.Initialize(); err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Create signature field and appearance. + opts := annotator.NewSignatureFieldOpts() + opts.FontSize = 10 + opts.Rect = []float64{10, 25, 75, 60} + + field, err := annotator.NewSignatureField( + signature, + []*annotator.SignatureLine{ + annotator.NewSignatureLine("Name", "John Doe"), + annotator.NewSignatureLine("Date", "2019.16.04"), + annotator.NewSignatureLine("Reason", "Signature test"), + }, + opts, + ) + field.T = core.MakeString("Test Sign LTV enable") + + if err = appender.Sign(1, field); err != nil { + log.Fatal("Fail: %v\n", err) + } + + // LTV enable the certificate chain used to apply the signature. + ltv, err := model.NewLTV(appender) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + if err := ltv.EnableChain(certChain); err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Write output PDF file. + if err = appender.WriteToFile(outputPath); err != nil { + log.Fatal("Fail: %v\n", err) + } + + log.Printf("PDF file successfully signed. Output path: %s\n", outputPath) +} diff --git a/signatures/ltv/pdf_sign_ltv_timestamp_revision.go b/signatures/ltv/pdf_sign_ltv_timestamp_revision.go new file mode 100644 index 00000000..3e4a289b --- /dev/null +++ b/signatures/ltv/pdf_sign_ltv_timestamp_revision.go @@ -0,0 +1,278 @@ +/* + * This example showcases how to digitally sign a PDF file using a + * PKCS12 (.p12/.pfx) file and LTV enable the signature by adding a second + * revision to the document (containing the validation data) and a timestamp + * signature (in order to protect the validation information). + * + * $ ./pdf_sign_ltv_timestamp_revision [] + */ + +package main + +import ( + "bytes" + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "time" + + "golang.org/x/crypto/pkcs12" + + "github.com/unidoc/unipdf/v3/annotator" + "github.com/unidoc/unipdf/v3/common/license" + "github.com/unidoc/unipdf/v3/core" + "github.com/unidoc/unipdf/v3/model" + "github.com/unidoc/unipdf/v3/model/sighandler" +) + +const licenseKey = ` +-----BEGIN UNIDOC LICENSE KEY----- +Free trial license keys are available at: https://unidoc.io/ +-----END UNIDOC LICENSE KEY----- +` + +const usage = "Usage: %s P12_FILE PASSWORD INPUT_PDF_PATH OUTPUT_PDF_PATH [EXTRA_CERTS]\n" + +func init() { + // Enable debug-level logging. + // unicommon.SetLogger(unicommon.NewConsoleLogger(unicommon.LogLevelDebug)) + + err := license.SetLicenseKey(licenseKey, `Company Name`) + if err != nil { + panic(err) + } +} + +func main() { + args := os.Args + if len(args) < 5 { + fmt.Printf(usage, os.Args[0]) + return + } + p12Path := args[1] + password := args[2] + inputPath := args[3] + outputPath := args[4] + + // Load private key and X509 certificate from the PKCS12 file. + pfxData, err := ioutil.ReadFile(p12Path) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + priv, cert, err := pkcs12.Decode(pfxData, password) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Load certificate chain. + certChain := []*x509.Certificate{cert} + if len(args) == 6 { + issuerCertData, err := ioutil.ReadFile(args[5]) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + for len(issuerCertData) != 0 { + var block *pem.Block + block, issuerCertData = pem.Decode(issuerCertData) + if block == nil { + break + } + + issuer, err := x509.ParseCertificate(block.Bytes) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + certChain = append(certChain, issuer) + } + } + + // Sign and write file to buffer. + signedBytes, err := signFile(inputPath, priv.(*rsa.PrivateKey), cert) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // LTV enable, timestamp and write file to buffer. + signedBytes, err = ltvEnableAndTimestamp(bytes.NewReader(signedBytes), certChain) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Optionally, LTV enable the applied timestamp signature. + signedBytes, err = ltvEnableTimestampSig(bytes.NewReader(signedBytes)) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + // Write output file to disk. + err = ioutil.WriteFile(outputPath, signedBytes, 0644) + if err != nil { + log.Fatal("Fail: %v\n", err) + } + + log.Printf("PDF file successfully signed. Output path: %s\n", outputPath) +} + +func signFile(inputPath string, priv *rsa.PrivateKey, cert *x509.Certificate) ([]byte, error) { + // Create reader. + file, err := os.Open(inputPath) + if err != nil { + return nil, err + } + defer file.Close() + + reader, err := model.NewPdfReader(file) + if err != nil { + return nil, err + } + + // Create appender. + appender, err := model.NewPdfAppender(reader) + if err != nil { + return nil, err + } + + // Create signature handler. + handler, err := sighandler.NewAdobePKCS7Detached(priv, cert) + if err != nil { + return nil, err + } + + // Create signature. + signature := model.NewPdfSignature(handler) + signature.SetName("Test Sign LTV enable") + signature.SetReason("TestSignLTV") + signature.SetDate(time.Now(), "") + + if err := signature.Initialize(); err != nil { + return nil, err + } + + // Create signature field and appearance. + opts := annotator.NewSignatureFieldOpts() + opts.FontSize = 10 + opts.Rect = []float64{10, 25, 75, 60} + + field, err := annotator.NewSignatureField( + signature, + []*annotator.SignatureLine{ + annotator.NewSignatureLine("Name", "John Doe"), + annotator.NewSignatureLine("Date", "2019.16.04"), + annotator.NewSignatureLine("Reason", "Signature test"), + }, + opts, + ) + field.T = core.MakeString("Test Sign LTV enable") + + if err = appender.Sign(1, field); err != nil { + return nil, err + } + + // Write output PDF file to buffer. + buf := bytes.NewBuffer(nil) + if err = appender.Write(buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func ltvEnableAndTimestamp(r io.ReadSeeker, certChain []*x509.Certificate) ([]byte, error) { + // Create reader. + reader, err := model.NewPdfReader(r) + if err != nil { + return nil, err + } + + // Create appender. + appender, err := model.NewPdfAppender(reader) + if err != nil { + return nil, err + } + + // LTV enable the certificate chain used to apply the signature. + ltv, err := model.NewLTV(appender) + if err != nil { + return nil, err + } + + if err := ltv.EnableAll(certChain); err != nil { + return nil, err + } + + // Create timestamp handler. + handler, err := sighandler.NewDocTimeStamp("https://freetsa.org/tsr", crypto.SHA512) + if err != nil { + return nil, err + } + + // Create signature field and appearance. + signature := model.NewPdfSignature(handler) + signature.SetName("Test Sign Timestamp") + signature.SetReason("TestSignTimestamp") + + if err := signature.Initialize(); err != nil { + return nil, err + } + + sigField := model.NewPdfFieldSignature(signature) + sigField.T = core.MakeString("Test Sign Timestamp") + sigField.Rect = core.MakeArray( + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + ) + + if err = appender.Sign(1, sigField); err != nil { + return nil, err + } + + // Write output PDF file to buffer. + buf := bytes.NewBuffer(nil) + if err = appender.Write(buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func ltvEnableTimestampSig(r io.ReadSeeker) ([]byte, error) { + // Create reader. + reader, err := model.NewPdfReader(r) + if err != nil { + return nil, err + } + + // Create appender. + appender, err := model.NewPdfAppender(reader) + if err != nil { + return nil, err + } + + // LTV enable the certificate chain used to apply the signature. + ltv, err := model.NewLTV(appender) + if err != nil { + return nil, err + } + + if err := ltv.EnableAll(nil); err != nil { + return nil, err + } + + // Write output PDF file to buffer. + buf := bytes.NewBuffer(nil) + if err = appender.Write(buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +}