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

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

Validating intent

<g:actionSubmit action="sign" value="I agree"
    title="I agree" tabindex="-1"
    style="position: relative;
        left: ${Math.floor(Math.random() * 600).intValue()}px;"/>

Do I just have an evil sense of humor? No. What makes an electronic signature legally valid is the conscious intent of the signer to sign. This is the submit button I’m using on a form to collect an electronic signature. It would be very difficult to click this button by accident. The random positioning also prevents many automated tools from clicking it. If a customer ever wants to contest that they have willingly accepted the terms of this electronic signature, I’ve just made it more difficult for them to prove their case in court.

Still, I can’t help getting an evil chuckle out of writing this code.

#electronic-signature, #gsp