Enabling LTV Using Second Revision And TimeStamp

This guide will demonstrate the process of digitally signing a PDF file using a PKCS12 (.p12/.pfx) file and LTV enabled the signature.

Before you begin

You should get your API key from your UniCloud account.

If this is your first time using UniPDF SDK, follow this guide to set up a local development environment.

Project setup

Clone the project repository

In your terminal, clone examples repository using the following command: It contains the Go code we will be using for this guide.

git clone https://github.com/unidoc/unipdf-examples.git

Then navigate to the signatures folder in the unipdf-examples directory.

cd unipdf-examples/signatures

Configure environment variables

Configure your license key using the following command: Replace the UNIDOC_LICENSE_API_KEY with your API credentials from your UniCloud account.

Linux/Mac

export UNIDOC_LICENSE_API_KEY=PUT_YOUR_API_KEY_HERE

Windows

set UNIDOC_LICENSE_API_KEY=PUT_YOUR_API_KEY_HERE

How it works

/*
* 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 <FILE.p12> <P12_PASS> <INPUT_PDF_PATH> <OUTPUT_PDF_PATH> [<EXTRA_CERTS.pem>]
*/
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"
)
func init() {
// Make sure to load your metered License API key prior to using the library.
// If you need a key, you can sign up and create a free one at https://cloud.unidoc.io
err := license.SetMeteredKey(os.Getenv(`UNIDOC_LICENSE_API_KEY`))
if err != nil {
panic(err)
}
}
const usagef = "Usage: %s P12_FILE PASSWORD INPUT_PDF_PATH OUTPUT_PDF_PATH [EXTRA_CERTS]\n"
func main() {
args := os.Args
if len(args) < 5 {
fmt.Printf(usagef, 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
}

The import section in lines 12-32, imports all the necessary dependencies. The init function in lines 34-41, authenticates UniPDF library request by setting the metered license key using license.SetMeteredKey(os.Getenv(`UNIDOC_LICENSE_API_KEY`)).

The main function is defined in lines 45-115. Inside this function the code in lines 51-54 gets the p12Path, password, inputPath and outputPath from the command line arguments. The private key and the certificate are loaded and decoded from the p12Path in lines 57-65. The next section loads the certificate chain from the command line arguments and decodes it as follows:

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)
  }
}

The document is signed and written to buffer in line 91. Then the buffer that contains the signed content is passed through the ltvEnableAndTimestamp function to enable LTV, timestamp and then write to buffer. Then at line 103, the timestamp signature is LTV enabled using ltvEnableTimestampSig(bytes.NewReader(signedBytes)).

Finally, the document is written to file using ioutil.WriteFile(outputPath, signedBytes, 0644) in line 109.

The signFile function in lines 117-179, is used to sign a file and return signed content as byte buffer. In this function in line 137, a new handler is created using sighandler.NewAdobePKCS7Detached(priv, cert). Then in line 142-146, a new PdfSignature object is created from the signature handler. Signature field and appearance is created in lines 153-166. Finally, the document is signed in line 168 and returned as a buffer at line 178.

The ltvEnableAndTimestamp function is used to create a timeStamp signature with LTV enabled. In line 189 of this function a new PdfAppender is created using model.NewPdfAppender(reader). Then a new model.LTV object is created in line 200. After this the LTV is enabled using ltv.EnableAll(certChain).

In line 205, The timestamp handler is created using handler, err := sighandler.NewDocTimeStamp("https://freetsa.org/tsr", crypto.SHA512). Then in lines 211-226, the PdfSignature and PdfSignatureField are created as follows:

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),
)

Using the Sign method of the Appender object, the document is signed in line 228. Finally, the signed document is returned as bytes.buffer in line 238.

The definition of the ltvEnableTimestampSig function starts in line 241. In line255, a new model.LTV object is created using ltv, err := model.NewLTV(appender). Then it is enabled in line 260 using ltv.EnableAll(nil). The document is returned as a buffer in line 270.

Run the code

To run the code use the following command:

go run pdf_sign_ltv_timestamp_revision.go <FILE.p12> <P12_PASS> <INPUT_PDF_PATH> <OUTPUT_PDF_PATH> [<EXTRA_CERTS.pem>]

Got any Questions?

We're here to help you.