Skip to content
Advertisement

Digitally Signing a PDF in Modern Firefox/Chrome/Edge Browsers

I have been down several rabbit holes looking for an answer for this.

I have a web application, written in AngularJS, that currently requires IE11 and the Acrobat plugin to digitally sign a PDF form. However, the plugin is garbage on IE11 and is not supported on modern browsers (which I define as Firefox, Chrome, and Edge. Safari will not be supported by my company.)

Because the application is AngularJS (and NOT running on Node), I need a javascript solution to sign the PDF. Not only that, but signature certificates are held on a smart card, meaning that I need a crypto library that can access the certs through some sort of PKCS#11 interface. In Javascript. Opening the forms externally in Acrobat is currently not acceptable to the customer.

I’ve looked at multiple libraries without being able to figure out a straight answer:

  • PKI.js
  • pkcs11.js
  • hwcrypto.js
  • graphene.js

None of these packages provide enough information to me to know whether to research them further.

Can anyone out there provide me further information or direction?

Thanks, Jason

Advertisement

Answer

This answer aims at workarounds, rather than actual answers.

This is because there’s no API in the browser at the moment you could use to obtain smart card certificate’s private key and use it. This has been supposedly discussed in the Web Crypto API and as far as I remember – the consensus was this should not be supported for security reasons (which I stronly disagree!).

You, as hundreds or thousands other developers (us included), are out of luck.

First workaround involves a .NET ClickOnce desktop app that is deployed at the server and run from the server. The app gets the security context of the current user session in runtime arguments so that the session is shared between the browser and the app that runs beside the browser. In this sense, running this app independently (without the session in the browser) would cause authorization issues during the communication with the server.

The app uses server’s APIs to first retrieve the document user is about to sign. Then then app uses the local certificate store with no restrictions (as it’s a regular desktop app), encrypts the document and sends it back to the server.

Pros: ClickOnce apps can be invoked from within the browser.

Cons: this requires .NET runtime at the client.

Second workaround involves a Java desktop app that is installed independently at the client’s machine. You provide install packages for selected OSes (say Windows, Linux, MacOS), the user downloads the install package and installs the app in their OS.

Then, when the browser is supposed to sign a document, you provide an instruction that tells the user to run the app in the background. The app, when run, exposes an HTTP listener on a localhost and fixed port with two services

  • a push service that accepts the document data to be signed
  • a pull service that exposes the signed document, when it’s available

As you guess, it’s the browser that does the request, the browser makes a request to the localhost:port and uploads the document data to the push service. The Java app switches from waiting for the document to signing the document state. The user is supposed to use the app – pick a cert from the store (no restrictions since is a regular Java desktop app) and sign the document. Your browser in the background pings the pull service of the app and when the data is ready, the browser downloads it. Then, the browser uploads the signed document to the actual server, using the actual authenticated session.

There’s a potential security hole here, as any local app or any opened web page could ping the pull service and download the document (which of course you don’t want). We are aware of two fixes to this.

First, you can have yet another service in the Java app that returns a one-time authentication token (a guid for example) that is meant to be read once and then supplied with every call to the pull service as an authentication token. If any other malicious app or webpage reads the token before your’s app web page does, your page will get errors from the pull service (as the one-time token has been apparently stolen and was not available). The web page could signal a communication error here and warn the user of a potential security issue.

Second way to fix the hole involve an argument to the pull service call that is provided by the application server and put in the page’s script as a value, a token signed by the server’s certificate. Your Java app can have the public key of the server’s certificate so that the Java app is able to verify the argument’s signature. But no other app (and no other page) will be able to forge the token (as the token’s signature’s private key is only available at your server) and there’s no easy way to steal the valid token from the page’s body.

Pros: the Java app could possibly target multiple OSes Cons: this still requires the Java runtime at the client

Both workarounds are tested in production and both work well for years. I hope this gives you one possible direction your final solution could be based on.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement