Sign with External Services

This guide will explain the process of digitally signing a PDF file using an external signing service which returns PKCS7 package. The task of an external service is simulated by signing the file with UniPDF.

Sample Input

sample PDF file

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

The next section will explain the example code.

How it works

/*
* This example showcases how to digitally sign a PDF file using an external
* signing service which returns PKCS7 package. The external service is
* is simulated by signing the file with UniDoc.
*
* $ ./pdf_sign_external <INPUT_PDF_PATH> <OUTPUT_PDF_PATH>
*/
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"fmt"
"log"
"math/big"
"os"
"time"
"github.com/unidoc/unipdf/v4/annotator"
"github.com/unidoc/unipdf/v4/common/license"
"github.com/unidoc/unipdf/v4/core"
"github.com/unidoc/unipdf/v4/model"
"github.com/unidoc/unipdf/v4/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)
}
}
var now = time.Now()
const usagef = "Usage: %s INPUT_PDF_PATH OUTPUT_PDF_PATH\n"
func main() {
args := os.Args
if len(args) < 3 {
fmt.Printf(usagef, os.Args[0])
return
}
inputPath := args[1]
outputPath := args[2]
// Generate PDF file signed with empty signature.
handler, err := sighandler.NewEmptyAdobePKCS7Detached(8192)
if err != nil {
log.Fatal("Fail: %v\n", err)
}
pdfData, signature, err := generateSignedFile(inputPath, handler)
if err != nil {
log.Fatal("Fail: %v\n", err)
}
// Parse signature byte range.
byteRange, err := parseByteRange(signature.ByteRange)
if err != nil {
log.Fatal("Fail: %v\n", err)
}
// This would be the time to send the PDF buffer to a signing device or
// signing web service and get back the signature. We will simulate this by
// signing the PDF using UniDoc and returning the signature data.
signatureData, err := getExternalSignature(inputPath)
if err != nil {
log.Fatal("Fail: %v\n", err)
}
// Apply external signature to the PDF data buffer.
// Overwrite the generated empty signature with the signature
// bytes retrieved from the external service.
sigBytes := make([]byte, 8192)
copy(sigBytes, signatureData)
sig := core.MakeHexString(string(sigBytes)).Write()
copy(pdfData[byteRange[1]:byteRange[2]], sig)
// Write output file.
if err := os.WriteFile(outputPath, pdfData, os.ModePerm); err != nil {
log.Fatal("Fail: %v\n", err)
}
log.Printf("PDF file successfully signed. Output path: %s\n", outputPath)
}
// generateSignedFile generates a signed version of the input PDF file using the
// specified signature handler.
func generateSignedFile(inputPath string, handler model.SignatureHandler) ([]byte, *model.PdfSignature, error) {
// Create reader.
file, err := os.Open(inputPath)
if err != nil {
return nil, nil, err
}
defer file.Close()
reader, err := model.NewPdfReader(file)
if err != nil {
return nil, nil, err
}
// Create appender.
appender, err := model.NewPdfAppender(reader)
if err != nil {
return nil, nil, err
}
// Create signature.
signature := model.NewPdfSignature(handler)
signature.SetName("Test External Signature")
signature.SetReason("TestAppenderExternalSignature")
signature.SetDate(now, "")
if err := signature.Initialize(); err != nil {
return nil, 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", "External signature test"),
},
opts,
)
field.T = core.MakeString("External signature")
if err = appender.Sign(1, field); err != nil {
return nil, nil, err
}
// Write PDF file to buffer.
pdfBuf := bytes.NewBuffer(nil)
if err = appender.Write(pdfBuf); err != nil {
return nil, nil, err
}
return pdfBuf.Bytes(), signature, nil
}
// getExternalSignature simulates an external service which signs the specified
// PDF file and returns its signature.
func getExternalSignature(inputPath string) ([]byte, error) {
// Generate private key.
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// Initialize X509 certificate template.
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test Company"},
},
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(time.Hour * 24 * 365),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
// Generate X509 certificate.
certData, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(certData)
if err != nil {
return nil, err
}
// Sign input file.
handler, err := sighandler.NewAdobePKCS7Detached(priv, cert)
if err != nil {
return nil, err
}
_, signature, err := generateSignedFile(inputPath, handler)
if err != nil {
return nil, err
}
return signature.Contents.Bytes(), nil
}
// parseByteRange parses the ByteRange value of the signature field.
func parseByteRange(byteRange *core.PdfObjectArray) ([]int64, error) {
if byteRange == nil {
return nil, errors.New("byte range cannot be nil")
}
if byteRange.Len() != 4 {
return nil, errors.New("invalid byte range length")
}
s1, err := core.GetNumberAsInt64(byteRange.Get(0))
if err != nil {
return nil, errors.New("invalid byte range value")
}
l1, err := core.GetNumberAsInt64(byteRange.Get(1))
if err != nil {
return nil, errors.New("invalid byte range value")
}
s2, err := core.GetNumberAsInt64(byteRange.Get(2))
if err != nil {
return nil, errors.New("invalid byte range value")
}
l2, err := core.GetNumberAsInt64(byteRange.Get(3))
if err != nil {
return nil, errors.New("invalid byte range value")
}
return []int64{s1, s1 + l1, s2, s2 + l2}, nil
}

The import section in lines 10-29, imports the necessary UniPDF packages and other libraries. The init function initializes the program by setting the metered license key.

The main function is defined in lines 44-93. In lines 45-51, the input path and the output path are parsed from the command line arguments. Then in line 59, the generateSignedFile function is used to get the signed version of the file by providing the input path and a model.SignatureHandler. The signature handler is created in line 54 using handler, err := sighandler.NewEmptyAdobePKCS7Detached(8192). Here the signature length is specified in the argument which is 8192. Then in line 65 the Byte range value of the signature field is parsed using the parseByteRange(signature.ByteRange) function. The external signing service is simulated in line 73 using getExternalSignature(inputPath) function.

In lines 81-90, the external signature obtained from an external signing service are applied to the PDF buffer using:

sigBytes := make([]byte, sigLen)
copy(sigBytes, signatureData)

sig := core.MakeHexString(string(sigBytes)).Write()
copy(pdfData[byteRange[1]:byteRange[2]], sig)

Then the document is written to file in line 88.

The generateSignedFile function which is used to generate the signed version of the provided document is defined in lines 97-153. In this function in lines 117-120, a model.PdfSignature is created from the signature handler provided in the function parameters. Then by calling the Initialize() method, the signature dictionary is prepared for signing. In lines 126-140, the signature field and appearance is created using the annotator function. This can be shown below.

// 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", "External signature test"),
  },
  opts,
)
field.T = core.MakeString("External signature")

After preparing the model.PdfFieldSignature this way, the document is signed using appender.Sign(1, field). Then the document is written to a buffer which then gets returned from the function.

The function getExternalSignature, which simulates an external signing service is defined in lines 157-201. First a private key is generated using rsa.GenerateKey(rand.Reader, 2048). Then an X509 certificate template is initialized by providing basic certificate information. A certificate data is created from the template using x509.CreateCertificate function. After that the data is parsed to get an x509.Certificate object. This is done using x509.ParseCertificate(certData) function call. By providing the private key and the certificate object, a new signature handler is created using sighandler.NewAdobePKCS7Detached(priv, cert) in line 190. Using this handler the signed file is generated by calling generateSignedFile function. Finally, the byte content of the model.PdfSignature is returned at the end of the function execution.

The function parseByteRange defined in lines 204-231, is used to parse the byte range value of the signature field.

Run the code

Run the code using the following terminal command.

go run pdf_sign_external.go <INPUT_PDF_PATH> <OUTPUT_PDF_PATH>

Sample output

Signed with External Service

Got any Questions?

We're here to help you.