Document Signing Service With Grails and iText

I’ve posted examples of document signing using iText and Grails previously. Here’s the latest iteration of the service and methods that we’re using in our production digital signature process. It works in conjunction with the KeyStoreService posted in the previous article.

This code is basically just a refactoring of the examples provided in iText in Action, 2nd Edition, Chapter 12, with a few accomodations for Groovy, Grails, and a SafeNet Luna HSM. If you are serious about using iText for digital signatures, I would definitely recommend that you buy the book.

package com.geekcredential.document

import com.geekcredential.common.output.PdfUtil
import com.geekcredential.crypto.KeyStoreService
import com.itextpdf.text.DocumentException
import com.itextpdf.text.pdf.AcroFields
import com.itextpdf.text.pdf.OcspClientBouncyCastle
import com.itextpdf.text.pdf.PdfDate
import com.itextpdf.text.pdf.PdfDictionary
import com.itextpdf.text.pdf.PdfName
import com.itextpdf.text.pdf.PdfPKCS7
import com.itextpdf.text.pdf.PdfReader
import com.itextpdf.text.pdf.PdfSignature
import com.itextpdf.text.pdf.PdfSignatureAppearance
import com.itextpdf.text.pdf.PdfStamper
import com.itextpdf.text.pdf.PdfString
import com.itextpdf.text.pdf.PdfWriter
import com.itextpdf.text.pdf.TextField
import com.itextpdf.text.pdf.TSAClient
import com.itextpdf.text.pdf.TSAClientBouncyCastle
import java.security.MessageDigest

class DocumentService {

    boolean transactional = false

    KeyStoreService keyStoreService

    private static final List<Integer> PDF_PERMISSIONS = [
            PdfWriter.ALLOW_PRINTING,
            PdfWriter.ALLOW_SCREENREADERS
    ]

    def encryptFile(File file) {
        PdfUtil.encryptFile(file, PDF_PERMISSIONS, keyStoreService.pdfOwnerPassword)
    }

    def encryptFile(byte[] content) {
        PdfUtil.encrypt(content, PDF_PERMISSIONS, keyStoreService.pdfOwnerPassword)
    }

    def applySignatures(String documentPath, String signedDocumentPath, Map signatureMap,
                        String reason, String location) {
        PdfReader reader = new PdfReader(documentPath, keyStoreService.pdfOwnerPassword.bytes)
        File signed = new File(signedDocumentPath)
        FileOutputStream fout = new FileOutputStream(signed)
        // Groovy thinks '\0' is a GString, so we have to be explicit and force it to char.
        PdfStamper stamper = PdfStamper.createSignature(reader, fout, '\0'.toCharacter().charValue(), null, true)

        // Write the 'signatures' entered by applicants to the appropriate fields in the document.
        // These aren't digital signature fields, they're just text fields.
        applyNames(stamper, signatureMap)

        // Apply digital signature
        Map credentials = keyStoreService.credentials
        PdfSignatureAppearance appearance = setAppearance(stamper, credentials, reason, location)
        setCryptoDictionary(appearance)
        // Estimate how much room the signed content will take up and reserve some space for it.
        int contentEstimated = 15000
        preClose(appearance, contentEstimated)
        byte[] encodedSig = generateDigitalSignature(appearance, credentials)
        if (contentEstimated + 2 < encodedSig.length)
            throw new DocumentException("Document signature failed: Not enough space reserved for content in the signed file")
        insertSignedContent(contentEstimated, encodedSig, appearance)
    }

    private def applyNames(PdfStamper stamper, Map signatureMap) {
        AcroFields form = stamper.getAcroFields()
        /*
        Understand form field caching: Setting up a cache for field appearances can improve iText's performance when
        filling PDFs that have a lot of fields.
        (See http://api.itextpdf.com/itext/com/itextpdf/text/pdf/AcroFields.html#setFieldCache(java.util.Map))
        */
        form.setFieldCache(new HashMap<String, TextField>())
        signatureMap.each { String field, String signature ->
            form.setField(field, signature)
        }
        // 'Flattening' eliminates the form fields and merges the contents directly into the rest of the PDF stream.
        stamper.setFormFlattening(true)
        stamper.setFullCompression()
    }

    private PdfSignatureAppearance setAppearance(
            PdfStamper stamper,
            Map credentials,
            String reason,
            String location
    ) {
        PdfSignatureAppearance appearance = stamper.getSignatureAppearance()
        appearance.setCrypto(credentials.privateKey, credentials.certificateChain, null, PdfSignatureAppearance.WINCER_SIGNED)
        appearance.setReason(reason)
        appearance.setLocation(location)
        appearance.setContact(keyStoreService.contactEmail)
        appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED)
        return appearance
    }

    TSAClient getTimeStampAuthorityClient() {
        TSAClient tsc = new TSAClientBouncyCastle(keyStoreService.timestampUrl)
        return tsc
    }

    private byte[] getOcsp() {
        Map credentials = keyStoreService.credentials
        OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(
                credentials.certificateChain[0],
                credentials.certificateChain[credentials.certificateChain.length - 1],
                keyStoreService.ocspUrl)
        int attempts = 0
        byte[] result = null
        // OCSP is optional, and a failure to get a response from the certificate status service should not halt the
        // signature from proceeding. Nevertheless, we give our best effort. If we see these failure messages coming
        // up in the logs frequently, then we need to complain to our service provider about their uptime.
        while (!result && attempts < 3) {
            try {
                attempts++
                result = ocspClient.getEncoded()
            } catch (Throwable t) {
                log.warn("The OCSP service at ${keyStoreService.ocspUrl} could not be contacted. Attempt #${attempts}", t)
            }
        }
        if (!result) {
            log.warn("The OCSP service at ${keyStoreService.ocspUrl} could not be contacted after 3 attempts. Document signing will proceed without confirming that our certificate is still valid")
        }
        return result
    }

    private def setCryptoDictionary(PdfSignatureAppearance appearance) {
        PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, new PdfName("adbe.pkcs7.detached"))
        dic.setReason(appearance.reason)
        dic.setLocation(appearance.location)
        dic.setContact(appearance.contact)
        dic.setDate(new PdfDate(appearance.getSignDate()))
        appearance.setCryptoDictionary(dic)
    }

    private byte[] getDigest(PdfSignatureAppearance appearance) {
        InputStream data = appearance.getRangeStream()
        MessageDigest messageDigest = MessageDigest.getInstance("SHA1")
        byte[] buf = new byte[8192]
        int n
        while ((n = data.read(buf)) > 0) {
            messageDigest.update(buf, 0, n)
        }
        byte[] hash = messageDigest.digest()
        return hash
    }

    private void preClose(PdfSignatureAppearance appearance, int contentEstimated) {
        HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>()
        exc.put(PdfName.CONTENTS, new Integer(contentEstimated * 2 + 2))
        appearance.preClose(exc)
    }

    private byte[] generateDigitalSignature(
            PdfSignatureAppearance appearance,
            Map credentials
    ) {
        byte[] hash = getDigest(appearance)
        if (!hash) log.error("Digital signature failure: digest could not be obtained from the PDF")
        Calendar cal = Calendar.getInstance()
        // Check certificate revocation status using OCSP
        byte[] ocsp = getOcsp()
        // Create the signature
        PdfPKCS7 sgn = new PdfPKCS7(credentials.privateKey, credentials.certificateChain, null, "SHA1", null, false)
        if (!sgn) log.error("Digital signature failure: unable to obtain credentials from hardware storage")
        byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, ocsp)
        if (!sh) log.error("Digital signature failure: could not generate authenticated attribute bytes")
        sgn.update(sh, 0, sh.length)
        TSAClient tsc = getTimeStampAuthorityClient()
        return sgn.getEncodedPKCS7(hash, cal, tsc, ocsp)
    }

    private void insertSignedContent(int contentEstimated, byte[] encodedSig, PdfSignatureAppearance appearance) {
        byte[] paddedSig = new byte[contentEstimated]
        System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length)
        PdfDictionary signedDictionary = new PdfDictionary()
        signedDictionary.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true))
        appearance.close(signedDictionary)
    }

}
package com.geekcredential.common.output

import com.itextpdf.text.pdf.PdfEncryptor
import com.itextpdf.text.pdf.PdfReader
import com.itextpdf.text.pdf.PdfWriter

class PdfUtil {

    static int sumPermissions(List<Integer> permissions){
      // Get bitwise sum of permissions
      int sum = (Integer) permissions[0]
      permissions.each {p ->
          sum = sum | p
      }
      return sum
    }

    /**
     * Encrypts a PDF and then returns it as a byte array for writing to http response.
     * @param byte[] content (PDF contents)
     * @param int[] permissions (an array of permission constants (see PdfWriter))
     * @param ownerPassword password to be applied
     */
    static byte[] encrypt(byte[] content, List<Integer> permissions, String ownerPassword) {
        ByteArrayOutputStream bout = new ByteArrayOutputStream()
        /* The userPassword and the ownerPassword can be null or have
           zero length. In this case the ownerPassword is replaced by
           a random string. */
        PdfEncryptor.encrypt(new PdfReader(content),
                             bout,
                             PdfWriter.STANDARD_ENCRYPTION_128,
                             null,
                             ownerPassword,
                             sumPermissions(permissions))
        return bout.toByteArray()
    }

    /**
     * Encrypts a PDF and then saves it.
     * @param File file
     * @param int[] permissions (an array of permission constants (see PdfWriter))
     * @param ownerPassword password to be applied
     */
    static void encryptFile(File file, List<Integer> permissions, String ownerPassword) {
        file.setBytes(encrypt(file.readBytes(), permissions, ownerPassword))
    }

}
Advertisements

#digital-signature, #electronic-signature, #grails, #hsm, #itext, #pdf

Accessing the Luna HSM Keystore

This is a code example for accessing credentials stored on a Luna SA HSM for use with digital signatures. This example is implemented as a Grails service.

package com.geekcredential.crypto

// The EnvironmentContext class reads out environment variables that are set in the Tomcat 
// conf/context.xml of whatever environment this class is running in. This enables us to 
// connect to different HSM partitions for development or production servers.
import com.geekcredential.common.ui.EnvironmentContext
import java.security.cert.Certificate
import java.security.PrivateKey
import java.security.Security
import java.security.KeyStore
import com.safenetinc.luna.LunaSlotManager

/*
 * The KeyStoreService provides a layer of abstraction between e-signing
 * resources and the code that uses them. It manages connections to an
 * external certificate storage device and caches certificates in memory to
 * reduce the number of times it is necessary to retrieve them. Other
 * information also used in the e-signing process is also made available
 * through the KeyStoreService as a convenience.
 */
class KeyStoreService {

    boolean transactional = true

    // Represents a connection to a hardware device. The Luna SA has 4 "slots", but three
    // of those are ports where a USB PED can be plugged in. Slot #1 is a handle to the
    // cryptographic acceleration &amp; storage device inside the Luna SA server.
    private LunaSlotManager hsmConnection

    private String getEnvironmentVariable(String variableName) {
        String value = EnvironmentContext.getEnv(variableName)
        if (!value || value == 'null') {
            throw new Exception("Environment variable '${variableName}' is not set. Please add it to the             environment configuration (Tomcat conf/context.xml if this application is running as a .war) and restart this application.")
        }
        return value
    }

    private String getPartitionName() {
        return getEnvironmentVariable("esignaturePartitionName")
    }

    private String getPartitionPassword() {
        return getEnvironmentVariable("esignaturePartitionPassword")
    }

    private String getPrivateKeyLabel() {
        return getEnvironmentVariable("esignaturePrivateKeyLabel")
    }

    private String[] getCaCertLabels() {
        return getEnvironmentVariable("esignatureCaCertLabels").split(";")
    }

    private String getCertLabel() {
        return getEnvironmentVariable("esignatureCertLabel")
    }

    String getContactEmail() {
        return getEnvironmentVariable("esignatureContactEmail")
    }

    String getTimestampUrl() {
        return getEnvironmentVariable("esignatureTimestampUrl")
    }

    String getOcspUrl() {
        return getEnvironmentVariable("esignatureOcspUrl")
    }

    String getPdfOwnerPassword() {
        return getEnvironmentVariable("esignaturePdfOwnerPassword")
    }

    void refreshHsmConnection() {
        try {
            resolveLunaSlotManagerInstance()
            hsmConnectionLogin()
        } catch (Throwable t) {
            log.fatal("Unable to login to the Hardware Storage Module (HSM). E-signing can't be completed without access to a certificate", t)
            throw new Exception("Unable to login to the Hardware Storage Module (HSM). E-signing can't be completed without access to a certificate", t)
        }
    }

    private void hsmConnectionLogin() {
        synchronized (hsmConnection) {
            if (!hsmConnection.loggedIn) {
                hsmConnection.login(partitionPassword)
            }
        }
    }

    private void resolveLunaSlotManagerInstance() {
        if (!hsmConnection) {
            hsmConnection = LunaSlotManager.getInstance()
        }
        if (!hsmConnection) {
            throw new Exception("LunaSlotManager did not return an instance.")
        }
    }

    Map getCredentials() {
        try {
            if (!Security.getProvider("LunaProvider")) {
                Security.addProvider(new com.safenetinc.luna.provider.LunaProvider())
            }
            refreshHsmConnection()
            KeyStore ks = KeyStore.getInstance("Luna")
            ks.load(null, null)

            Map credentials = [:]
            credentials.put("privateKey", (PrivateKey) ks.getKey(privateKeyLabel, null))

            // We need to assemble the certificate chain manually because the HSM doesn't support the
            // getCertificateChain method. The array of certificates in the chain should be ordered
            // starting with our cert and then proceeding to any intermediate certificate authority certs
            // up to the original issuer.
            List chain = [ks.getCertificate(certLabel)]
            caCertLabels.each {label -&gt;
                chain &lt;&lt; ks.getCertificate(label)
            }
            credentials.put("certificateChain", chain.toArray(new Certificate[chain.size()]))

            return credentials
        } catch (Throwable t) {
            log.fatal("Unable to retrieve resources from the Hardware Storage Module (HSM). E-signing can't be completed without a private key and certificate chain", t)
            throw new Exception("Unable to retrieve resources from the Hardware Storage Module (HSM). E-signing can't be completed without a private key and certificate chain", t)
        }
    }

}

#digital-signature, #grails, #hsm, #luna-sa

Luna SA HSM Concepts

A Hardware Storage Module (HSM) is a more secure alternative to the keystore file most Java developers are familiar with. The HSM stores cryptographic data such as private keys and certificates in RAM or flash storage. Through software APIs, the Luna HSM participates in cryptographic operations like digital signatures without actually releasing the secured credentials to your applications, where they potentially could be exported or misused.

This was an important point for me to understand. Initially, I thought that I could get a private key and certificate chain from my Luna HSM at application startup, just like a keystore file, and then cache the credentials in memory for when I needed them. But it doesn’t work that way: the Luna API objects only give you pointers to data stored on the HSM; not the data themselves. My ‘cached certificates’ only resulted in NullPointerExceptions because the connections had expired by the time I tried to use them.

Most HSM devices include extra-paranoid security features to ensure that the data they hold does not fall into hostile hands. In addition to requiring authentication to use stored objects, the Luna SA includes tamper-detection features like a chassis opening sensor and an internal thermometer. If an intrusion is detected, the HSM erases its storage to prevent sensitive data from being stolen. You will want to make sure that all of your data center admins are aware of these features to prevent accidents!

The Luna SA also includes dedicated hardware for accelerated cryptographic calculations. Some models have high availability features.

Administration

There are multiple layers of systems on a Luna SA:

  • The appliance operating system
  • The HSM card
  • Logical partitions created within the HSM

The operating system’s root user is named ‘admin’. Admin can manage the device’s base configuration: networking, time, hardware settings. You can give the admin password to to data center staff who maintain the appliance, because the admin user has no authorization to access the HSM contained within.

The HSM itself is installed in a ‘slot’ inside the Luna SA. (I don’t know if ‘slot’ refers to a physical slot for an expansion card or just a logical interface to the HSM hardware. On the Luna SA 5, the built-in HSM is in slot 1 and slots 2 – 4 are available for backup devices to plug into.) The HSM has its own administrator, apart from the OS admin. It is sometimes referred to as the ‘security officer’ or ‘SO’ in documentation. You can manage the HSM by first logging into the Luna as the OS admin and then running the hsm command and supplying the security officer password.

Within the HSM, the SO can configure logical partitions for storage of crypto artifacts. Each partition has its own password. So one Luna SA can be used by multiple groups or applications. At my company, we’ve created one partition for certificates used by developers and another for production. That satisfies the regulatory requirement for separation of duties and controls between development and production personnel.

Some administration functions are done from a command line on the Luna. Others need to be performed remotely, using the command line tools that are provided with the client software.

Luna SA Setup

Turning on the appliance for the first time and configuring networking is pretty straightforward and is explained well by the documentation, so I don’t think it’s necessary for me to cover it. The only questions our network admin had for me related to some HSM-specific terms in the documentation.

Cloning refers the the backup process for the HSM. To permit the contents of the HSM to be backed up to a USB token, you need to enable cloning and assign the same cloning domain name to both the HSM and the backup token.

PIN means ‘password’ – at least in our configuration. If you have configured FIPS 140-2 Level 3 security, then the authentication process is different.

Manufacturer Documentation

Complete documentation, including a Getting Started guide and a full listing of options for all of the commands used below, is included with the Luna SA client software, in the form of a collection of HTML pages which you can unzip into a local directory. In addition, once you have the client software installed, you will find a Java API Guide and example code in the Program Files\LunaSA\JSP directory on your computer.

Installing Client Software

Instructions are provided for installing and configuring on Windows and AIX, since those are the operating systems I have worked with. The process for Linux or other UNIX breeds is probably pretty similar to AIX.

Tips for installing the client software:

Windows

  • The main installer launches several other installs before it completes. If you’re developing in Java or Groovy, you need the Java Security Provider (JSP). If you’re developing in C, you need the C Security Provider (CSP).
  • You may need to add C:\Program Files\LunaSA\JSP\lib to PATH in your Windows environment variables. (This puts LunaAPI.dll on java.library.path. You can run groovy -e “println System.getProperty(‘java.library.path’)” from a command line to verify what’s in java.library.path.)
  • Make sure there is a Windows environment variable named “ChrystokiConfigurationPath” and add it if it is missing. The value should indicate the location of the crystoki.ini file, which should be “C:\Program Files\LunaSA” in the default installation.

AIX

  • Before starting the installation, ensure that you have a Random Number Generator (RNG) or Entropy Gathering Daemon (EGD) on your system at one of /dev/egd-pool, /etc/egd-pool, /etc/entropy, or /var/run/egd-pool.
  • If you’re developing in Java or Groovy, answer ‘Y’ to install the Java Security Provider (JSP) and ‘Y’ to install the SDK.
  • Following installation, it is necessary to configure any environment where the Luna client is going to be used so that the Java runtime can interface with the native libLunaAPI.so library. Some of these methods for setting java.library.path are redundant. If you don’t have libLunaAPI.so on java.library.path, it will result in
    java.lang.UnsatisfiedLinkError: LunaAPI_64 (Not found in java.library.path)
    • Add /usr/lunasa/jsp/lib/ to the PATH environment variable
    • Add -Djava.library.path=/usr/lunasa/jsp/lib/ to the JAVA_OPTS environment variable
    • Add “/usr/lunasa/jsp/lib/” to the LIBPATH environment variable
    • Include /usr/lunasa/jsp/lib/LunaProvider.jar in your CLASSPATH

Registering a Client with the Luna HSM

The client software communicates with the HSM using SSH. Before you can start working with the crypto features, you will need to set up a trust between your client and the HSM by exchanging keys.

My experience has indicated that when a command requires you to specify a hostname you should provide only the unqualified hostname, without the domain name, for the server or client. The tools create files using the hostnames you enter, and in some cases, extra periods in the filenames seem to cause issues. Also, be consistent with case when you type the hostnames. They are added to configuration files and certificates exactly as you type them.

  • On the client, open a command prompt and cd to the directory where the client software is installed. On Windows that will be C:\Program Files\LunaSA. On AIX that will probably be /usr/lunasa/bin.
  • Download a copy of the server’s public key. On AIX, you can use the built in secure copy (scp) command. For Windows, PuTTY pscp is included with the client software.
    • Windows:
      scp admin@<hsm_hostname>:server.pem ../cert/server
    • AIX:
      pscp admin@<hsm_hostname>:server.pem ./cert/server
  • Configure your client to trust the server. This step uses the vtl client command.
    • Windows:
      vtl addServer -n <hsm_hostname> -c cert/server/server.pem
    • AIX:
      ./vtl addServer -n <hsm_hostname> -c ../cert/server/server.pem
  • Generate a private key for your client. Running this command will cause two files to be created in LunaSA/cert/client.
    • Windows:
      vtl createCert -n <client_hostname>
    • AIX:
      ./vtl createCert -n <client_hostname>
  • Export the client cert you generated and transmit it to the server. Note that there is a colon on the end of the server’s hostname, designating that you want to copy your client certificate to the root directory on the server. If you leave the colon off, it will result in the error message “Local to local copy not supported”.
    • Windows:
      pscp "cert\client\<client_hostname>.pem" admin@<hsm_hostname>:
    • AIX:
      scp ../cert/client/<client_hostname>.pem admin@<hsm_hostname>:
  • On the hsm command line, associate the key file to your client by hostname. This command registers the client by its DNS hostname. If your client isn’t found in DNS, the HSM can’t see it.
    client register -client <client_hostname> -hostname <client_hostname>

  • Assign the client to a partition. (Note that you can get a list of partition names by issuing the partition list command.) You will need to know the partition password.
    • On the hsm command line:
      client assignPartition -client <client_hostname> -partition <partition_name>
    • Confirm the setup. On the client command line:
      vtl verify

Requesting and Importing Certificates

  • On the client, open a command prompt and cd to the directory where the client software is installed. On Windows that will be C:\Program Files\LunaSA. On AIX that will probably be /usr/lunasa/bin.
  • Generate public and private keys using the certificate management utility, cmu. Once you have objects stored on the HSM, you will reference them by either numeric handle or text label. It’s best to give the keys descriptive labels, such as “developer_private_key” or “widgetco_inc_public_key”. The handles are just numeric ids assigned automatically when you upload objects.
    cmu generatekeypair -modulusbits=2048 -publicexponent=65537 -sign=T -verify=T -labelpublic="<public_key_label>" -labelprivate="<private_key_label>"
  • Create a certificate request. You can use the cmu list command to show a list of objects stored on the HSM, including the handle and label assigned to each. If there is only one keypair on the partition, the following command defaults to it. If there are more than one, you must also specify -publichandle and -privatehandle options.
    cmu requestcertificate -c="<two_letter_country_code>" -o="<organization_name>" -cn="<common_name>" -s="<state>" -l="<city/locality>" -publichandle=<public_handle> -privatehandle=<private_handle> -outputfile=""
  • The next step depends on the procedure that your certificate authority uses. But it will probably go something like this: Go to the certificate authority’s website and follow their procedures to request a new certificate. At the point when you are given a chance to supply a certificate request of your own, open the CSR file you generated and paste the text from it into the certificate request window. Download the certificate files that are generated from it, following the website’s instructions. When you are done, you should have a certificate file and one or more CA certificate files downloaded to your client.
  • Import each certificate into the HSM partition. Assign each file that you import a meaningful, unique label.
    cmu import -inputFile="<filepath_and_filename_of_cert>" -label="<certificate_label>"
  • If, for any reason, you need to change a label or other attribute after import, you can use the cmu setAttribute command.
    cmu list

    cmu setAttribute -handle=<handle> -label=<new_label>

#cryptography, #digital-signature, #hsm, #luna-sa

Developing an Electronic Signature Solution with Grails, iText and Luna SA

It has been some time since I wrote about our efforts to develop a digital signature process. Now that we have a pilot release of our application in production, it seems like a good time to share what I’ve learned since then.

For our electronic signature solution, users fill in a series of HTML forms to apply for one of our products and then their input is used to generate a PDF version of the regulatory agency-approved application form. After obtaining the users’ confirmation that the PDF is an accurate reproduction of their responses and gaining their agreement to participate in an electronic signature process, we collect the participants’ typed names as their signatures, overwrite the PDF with their names on the appropriate lines, and digitally sign the PDF so that it can’t be altered and will stand as a legal record of the contract. The signed PDF is redisplayed to the users for their final approval before submission to back office systems.

Our digital signature solution was developed using Grails and iText.

Early in our development, we learned that while you can digitally sign a PDF with any certificate that has the right flags enabled, Adobe Reader will not reliably validate the signature upon opening (displaying the blue ribbon for a valid signature) unless you use a certificate that is chained from Adobe’s certificate authority or is on Adobe’s Approved Trust List.

Adobe’s certificate programs require partners to meet FIPS 140-2 level 2 or 3 security requirements, which include storing the certificate on a secured hardware device that can only be accessed using appropriate credentials at the time of signature. Needless to say, accepting Adobe’s solution significantly increased the cost of our project, but it was deemed to be necessary to retain customer trust in the electronic signature.

We purchased a document signing certificate from GlobalSign, which was delivered with a Luna SA 5 Hardware Storage Module from SafeNet.

We did consider using a PCI card version of the HSM instead of the more expensive networked appliance, but unfortunately, our production server is a blade and has no PCI slots. Also, we needed to be able to sign from developers’ Windows laptops and from our continuous integration server, and the PCI solution would not be accessible from multiple computers.

Because the Adobe certificate and the HSM were expensive, we made sure to verify that the solution would work as advertised before signing the purchase order. GlobalSign offers a 90-day trial certificate and SafeNet made available to us a Luna SA hosted from their location that they call the e-Lab. So we were actually able to develop and test proof of concept code on our own hardware before we made the purchase.

In future posts, I’ll share some info about how we got the Luna SA set up and loaded with our certificates, and give some code examples of how to sign using the Luna SA 5 and iText.

#adobe-cds, #digital-signature, #electronic-signature, #grails, #hsm, #itext, #luna-sa

Signature With Timestamp Using iText and Luna HSM


import java.security.KeyStore
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.cert.Certificate
import com.itextpdf.text.pdf.PdfReader
import com.itextpdf.text.pdf.PdfStamper
import com.itextpdf.text.pdf.PdfSignatureAppearance
import com.itextpdf.text.pdf.PdfName
import com.itextpdf.text.Image
import com.itextpdf.text.Rectangle
import com.itextpdf.text.pdf.PdfEncryptor
import com.itextpdf.text.pdf.PdfWriter
import com.itextpdf.text.pdf.PdfSignature
import com.itextpdf.text.pdf.PdfDate
import com.itextpdf.text.pdf.TSAClient
import com.itextpdf.text.pdf.TSAClientBouncyCastle
import com.itextpdf.text.pdf.PdfPKCS7
import com.itextpdf.text.DocumentException
import com.itextpdf.text.pdf.PdfDictionary
import com.itextpdf.text.pdf.PdfString
import com.chrysalisits.crypto.LunaTokenManager



String hsmPartitionLabel = "Luna_partition_name"
String hsmPassword = "partition_password"
String hsmKeyLabel = "private_key_alias"
String hsmCertLabel = "certificate_alias"
String hsmCALabel = "CA_certificate_alias"
String timestampUrl = "URL_of_timestamp_server"
String ownerPassword = "PDF_owner_password"
String inFile = "path_to_unsigned_PDF"
String reason = "signature_reason"
String location = "signature_location"
String contact = "signature_contact_email"
String sealPath = "path_to_an_image_to_be_used_with_signature"
String outFile = "path_to_put_signed_PDF"

// Login to HSM
LunaTokenManager tm = LunaTokenManager.getInstance()
tm.Login(hsmPartitionLabel, hsmPassword)

// Dynamically load security providers
Class providerClass = Class.forName("com.chrysalisits.crypto.LunaJCAProvider")
java.security.Provider provider = (java.security.Provider)providerClass.newInstance()
java.security.Security.removeProvider(provider.getName())
java.security.Security.insertProviderAt(provider, 2)
providerClass = Class.forName("com.chrysalisits.cryptox.LunaJCEProvider")
provider = (java.security.Provider) providerClass.newInstance()
java.security.Security.removeProvider(provider.getName())
java.security.Security.insertProviderAt(provider, 3)

// This syntax gets an instance of a LunaKeystore
KeyStore ks = KeyStore.getInstance("Luna")
ks.load(null, null)
PrivateKey key = (PrivateKey) ks.getKey(hsmKeyLabel, null)
// We need to assemble the certificate chain manually because the HSM doesn't support the
// getCertificateChain method.
Certificate[] chain = new Certificate[2]
chain[0] = ks.getCertificate(hsmCertLabel)
chain[1] = ks.getCertificate(hsmCALabel)

// It seems necessary to load the file into the PdfReader this way to
// avoid a java.io.IOException in sun.nio.ch.FileChannelImpl on AIX.
byte[] content = new File(inFile).readBytes()
PdfReader reader = new PdfReader(content, ownerPassword.getBytes())
FileOutputStream fout = new FileOutputStream(outFile)
// Third param is PDF revision (char).
// Groovy thinks '' is a GString, so we have to be explicit and force it to char.
PdfStamper stp = PdfStamper.createSignature(reader, fout, ''.toCharacter().charValue(), null, true)
PdfSignatureAppearance sap = stp.getSignatureAppearance()
// Instead of reason and location, a graphic image will be rendered. Reason and
// location will still be shown in the signature properties.
sap.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC)
Image image = Image.getInstance(sealPath)
sap.setSignatureGraphic(image)
// sap.setVisibleSignature(new Rectangle(x-len, y-len, x-loc, y-loc), page, null for new fieldname)
// Coordinates begin from lower left. Units are 1/72 of an inch. 8.5 x 11 in == 612 x 792
sap.setVisibleSignature(new Rectangle(36, 36, 100, 100), 1, null)
sap.setCrypto(key, chain, null, PdfSignatureAppearance.WINCER_SIGNED)

PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, new PdfName("adbe.pkcs7.detached"))
dic.setReason(reason)
dic.setLocation(location)
dic.setContact(contact)
dic.setDate(new PdfDate(sap.getSignDate()))
sap.setCryptoDictionary(dic)

// This is estimated space for the signature itself.
int contentEstimated = 15000
HashMap exc = new HashMap()
exc.put(PdfName.CONTENTS, new Integer(contentEstimated * 2 + 2))
sap.preClose(exc)

// make the digest
InputStream data = sap.getRangeStream()
MessageDigest messageDigest = MessageDigest.getInstance("SHA1")
byte[] buf = new byte[8192]
int n
while ((n = data.read(buf)) > 0) {
messageDigest.update(buf, 0, n)
}
byte[] hash = messageDigest.digest()
Calendar cal = Calendar.getInstance()

// If we add a time stamp:
TSAClient tsc = null
String tsa_url = timestampUrl
// Our provider does not use userid and password; use
// TSAClientBouncyCastle(tsa_url, tsa_userid, tsa_password)
// if yours does.
tsc = new TSAClientBouncyCastle(tsa_url)

byte[] ocsp = null

// Create the signature
PdfPKCS7 sgn = new PdfPKCS7(cert.key, cert.chain, null, "SHA1", null, false)
byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, ocsp)
sgn.update(sh, 0, sh.length)
byte[] encodedSig = sgn.getEncodedPKCS7(hash, cal, tsc, ocsp)

if (contentEstimated + 2 < encodedSig.length)
throw new DocumentException("Not enough space")

byte[] paddedSig = new byte[contentEstimated]
System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length)
// Replace the contents
PdfDictionary dic2 = new PdfDictionary()
dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true))
sap.close(dic2)

tm.Logout()

println "Signed PDF saved as ${outFile}."

#adobe-cds, #digital-signature, #groovy, #hsm, #itext, #luna, #pdf, #timestamp

Some questions for the iText community

ArrayIndexOutOfBoundsException when signing Documaker PDF

Certificate vendors for digital signatures

#certificate, #digital-signature, #itext, #pdf

How to sign a PDF using iText and Groovy

Paulo Soares has posted some very good examples of how to sign a PDF using iText. (iText is an open source library for working with PDFs.) I ran into a few hiccups trying to implement his example in Groovy, so I thought I’d share my fixes.

Here is a class with a method for signing PDFs. It includes the imports! This example presumes that the PDF is already encrypted to require a password for editing and that a signature field has already been placed in the PDF.

package com.geekcredential.common.output

import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.Certificate
import java.security.MessageDigest
import com.itextpdf.text.pdf.PdfReader
import com.itextpdf.text.pdf.PdfStamper
import com.itextpdf.text.pdf.PdfSignatureAppearance
import com.itextpdf.text.pdf.PdfSigGenericPKCS
import com.itextpdf.text.pdf.PdfLiteral
import com.itextpdf.text.pdf.PdfPKCS7
import com.itextpdf.text.pdf.PdfDictionary
import com.itextpdf.text.pdf.PdfString
import com.itextpdf.text.pdf.PdfName
import org.apache.log4j.Logger

/**
 * Utilities for working with PDF files.
 */

public class PdfUtil {

    /**
     * @param String keyFile (filepath to PKCS12 certificate)
     * @param String keyfilePassword
     * @param String inFile (filepath to PDF to be signed)
     * @param String ownerPassword (password for changing encrypted PDF)
     * @param String reason (text of Reason field in signature)
     * @param String location (text of Location field in signature)
     * @param String sigFieldName (name of signature field in PDF)
     * @param String outFile (desired name of signed file)
     */
    static String sign(
        String keyFile,
        String keyFilePassword,
        String inFile,
        String ownerPassword,
        String reason,
        String location,
        String sigFieldName,
        String outFile
    ) {
        Logger log = Logger.getLogger("com.geekcredential.common.output.PdfUtil")
        log.debug("Entering sign()")
        try {
            // Load the certificate to be used for signing
            KeyStore ks = KeyStore.getInstance("pkcs12")
            ks.load(new FileInputStream(keyFile), keyFilePassword.toCharArray())
            // Defaults here to first alias found in keyfile. We could change this
            // to specify a named alias.
            String alias = (String)ks.aliases().nextElement()
            PrivateKey key = (PrivateKey)ks.getKey(alias, keyFilePassword.toCharArray())
            Certificate[] chain = ks.getCertificateChain(alias)

            PdfReader reader = new PdfReader(inFile, ownerPassword.getBytes())
            FileOutputStream fout = new FileOutputStream(outFile)
            // Third param is PDF revision (char). Fifth param (boolean) enables append without
            // incrementing revision number.
            // Groovy thinks '\0' is a GString, so we have to be explicit and force it to char.
            PdfStamper stp = PdfStamper.createSignature(reader, fout, '\0'.toCharacter().charValue(), null, true)
            PdfSignatureAppearance sap = stp.getSignatureAppearance()
            sap.setCrypto(key, chain, null, PdfSignatureAppearance.SELF_SIGNED)
            sap.setReason(reason)
            sap.setLocation(location)
            sap.setVisibleSignature(sigFieldName)
            stp.close()
            return outFile
        } catch (Throwable t) {
            log.fatal("Failure signing PDF", t)
            throw t
        } finally {
            log.debug("Exiting sign()")
        }
    }
}

Note: iText supports three certificate signing modes: SELF_SIGNED, WINCER_SIGNED and VERISIGN_SIGNED. I could find no trace of the Verisign Adobe Reader plugin that is supposed to require VERISIGN_SIGNED on Verisign’s website, so I presume that this is old technology, discontinued. However, our Verisign test certificate works just fine with WINCER_SIGNED, and as Paulo Soares points out, WINCER_SIGNED mode works with any certificate. So, in practice, it seems that there are really only two modes: SELF_SIGNED or WINCER_SIGNED.

#digital-signature, #groovy, #itext, #pdf