Pular para o conteúdo principal

Signature sample explained - PKI Express on Java

This article explains how the digital signature process performed in the PKI Express Java Samples project works.

Main code files​

Signature process​

1. User goes to the signature page​

At this time, the user visits the URL /pades-signature-express?fileId={fileToSign} (GET). The controller PadesSignatureExpressController is responsible for responding to the request, more specifically in the method get():

@Controller
@RequestMapping("/pades-signature-express")
public class PadesSignatureExpressController {

/**
* GET /pades-signature-express?fileId={fileToSign}
*
* This action simply renders the page.
*/
@GetMapping
public String get(@RequestParam(value = "fileId") String fileToSign) throws IOException {

// Verify if the fileToSign exists.
if (fileToSign == null || !StorageMock.exists(fileToSign)) {
throw new FileNotFoundException();
}

// Render the signature page (templates/pades-signature-express/index.html)
return "pades-signature-express/index";
}

view on source

At this time, we just check that the file to be signed exists and render the signature page, the view pades-signature-express/index.

note

The parameter fileId and the class StorageMock are merely details of the example: they stand for the way your application stores its files.

Once the page has loaded, we pass references to the page elements to the JavaScript module signatureStartForm:

$(document).ready(function () {
// Once the page is ready, we call the init() function on the javascript code
// (see signature-start-form.js).
signatureStartForm.init({
form: $('#signForm'), // The form that should be submitted when the operation is complete.
certificateSelect: $('#certificateSelect'), // The <select> element (combo box) to list the certificates.
refreshButton: $('#refreshButton'), // The "refresh" button.
signButton: $('#signButton'), // The button that initiates the operation.
certThumbField: $('#certThumbField'), // The "certThumb" field.
certContentField: $('#certContentField') // The "certContent" field.
});
});

view on source

The module signatureStartForm blocks the screen and triggers Web PKI initialization, passing the function loadCertificates() as callback when the initialization has completed, and the function onWebPkiError() as defaultFail, the callback for errors on any later Web PKI operation:

// ----------------------------------------------------------------------------------------------
// Initializes the signature form.
// ----------------------------------------------------------------------------------------------
function init(fe) {

// Receive form parameters received as arguments.
formElements = fe;

// Wireup of buttons clicks.
formElements.signButton.click(startSignature);
formElements.refreshButton.click(refresh);

// Block the UI while we get things ready.
$.blockUI({ message: 'Initializing ...' });

// Call the init() method on the LacunaWebPKI object, passing a callback for when the
// component is ready to be used and another to be called when an error occurs on any of the
// subsequent operations. For more information, see:
// https://docs.lacunasoftware.com/en-us/articles/web-pki/get-started.html#coding-the-first-lines
// https://webpki.lacunasoftware.com/Help/classes/LacunaWebPKI.html#method_init
pki.init({
ready: loadCertificates, // As soon as the component is ready we'll load the certificates.
defaultFail: onWebPkiError // Generic error callback (see function declaration below).
});
}

view on source

In the function loadCertificates(), we trigger the loading of the certificates available on the user's computer, populating the element <select> from the page:

// ----------------------------------------------------------------------------------------------
// Function that loads the certificates, either on startup or when the user clicks the "Refresh"
// button. At this point, the UI is already blocked.
// ----------------------------------------------------------------------------------------------
function loadCertificates() {

// Call the listCertificates() method to list the user's certificates.
pki.listCertificates({

// ID of the <select> element to be populated with the certificates:
selectId: formElements.certificateSelect.attr('id'),

// Function that will be called to get the text that should be displayed for each option:
selectOptionFormatter: function (cert) {
var s = cert.subjectName + ' (issued by ' + cert.issuerName + ')';
if (new Date() > cert.validityEnd) {
s = '[EXPIRED] ' + s;
}
return s;
}

}).success(function () {

// Once the certificates have been listed, unblock the UI.
$.unblockUI();

});

}

view on source

2. User clicks the Sign File button​

Previously, the JavaScript module signatureStartForm registered JavaScript function startSignature() to be called when the Sign File button is clicked:

// Wireup of buttons clicks.
formElements.signButton.click(startSignature);
formElements.refreshButton.click(refresh);

view on source

note

It is critical that the button on the page has type="button", not type="submit", otherwise a postback would occur immediately upon clicking the button.

Therefore, when the user clicks the Sign File button, the startSignature() function of signatureStartForm is invoked:

// ----------------------------------------------------------------------------------------------
// Function called when the user clicks the "Sign File" button.
// ----------------------------------------------------------------------------------------------
function startSignature() {

// Block the UI while we perform the signature.
$.blockUI({ message: 'Starting signature ...' });

// Get the thumbprint of the selected certificate.
var selectedCertThumbprint = formElements.certificateSelect.val();

// Get certificate content to be passed to "start" action after the form submission.
pki.readCertificate(selectedCertThumbprint).success(/* ... */);
}

view on source

In this function, after blocking the page, we get the thumbprint of the selected certificate, which will be needed again later in the signature process. We then invoke the function readCertificate() of Web PKI to obtain the encoding of the chosen certificate (public part of the certificate, the ".cer", without the private key):

// Get certificate content to be passed to "start" action after the form submission.
pki.readCertificate(selectedCertThumbprint).success(// ...

view on source

Once the certificate is read, we fill in the hidden fields of the page with the thumbprint and the certificate, and do the postback:

/* ... */.success(function (certEncoded) {

// Fill fields needed on the next steps of the signature.
formElements.certThumbField.val(selectedCertThumbprint);
formElements.certContentField.val(certEncoded);

// Submit the form.
formElements.form.submit();

});

view on source

3. Signing backend start​

In the backend, the controller PadesSignatureExpressController is again invoked to handle the postback made from the frontend with the certificate chosen to perform the signature, done for the URL /pades-signature-express/start with a verb POST. The method postStart() is responsible for handling the request:

/**
* POST /pades-signature-express/start?fileId={fileToSign}
*
* This action receives the form submission from the signature page. It will perform a PAdES
* signature in three steps using PKI Express and Web PKI.
*/
@PostMapping("/start")
public String postStart(
@RequestParam(value = "fileId") String fileToSign,
@RequestParam String certThumb,
@RequestParam String certContent,
Model model
) throws IOException {
// ...

view on source

At this point, we start the signature using the class PadesSignatureStarter, passing the signature policy, the file to be signed and the encoding of the certificate chosen for signature:

// Get an instance of the PadesSignatureStarter class, responsible for receiving
// the signature elements and start the signature process.
PadesSignatureStarter signatureStarter = new PadesSignatureStarter();

// Set PKI default options (see Util.java)
Util.setPkiDefaults(signatureStarter);

// Set signature policy.
signatureStarter.setSignaturePolicy(StandardSignaturePolicies.PadesBasicWithLTV);

// Set PDF to be signed.
signatureStarter.setPdfToSign(StorageMock.getDataPath(fileToSign));

// Set Base64-encoded certificate's content to signature starter.
signatureStarter.setCertificateBase64(certContent);

view on source

The class is created without arguments, so it uses the default PkiExpressConfig: PKI Express in its default installation folder and the system temporary folder (see client library settings). Util.setPkiDefaults() sets what every operation of the sample has in common: the trusted roots, including the Lacuna test root when configured (trusting in custom certificates), the offline mode and the timestamp authority.

We then configure the signature appearance:

// Set visual representation. We provide a Java class that represents the visual
// representation model.
signatureStarter.setVisualRepresentation(PadesVisualElements.getVisualRepresentation());

view on source

note

For more details on configuration options for the appearance of the signature, see the class PadesVisualElements and the article PAdES visual representation.

Finally, we conclude the initial step, obtaining the necessary data to perform the digital signature algorithm, and delete the temporary files that the starter created:

// Start the signature process. Receive as response a SignatureStartResult instance
// containing the following fields:
// - toSignHash: The hash to be signed.
// - digestAlgorithm: The digest algorithm that will inform the Web PKI component
// to compute the signature.
// - transferFile: A temporary file to be passed to "complete" step.
SignatureStartResult result = signatureStarter.start();

// If you want to delete the temporary files created by this step use the method
// dispose(). This method MUST be called after the start() method, because it
// deletes some files needed by the method.
signatureStarter.dispose();

view on source

An important point: the class PadesSignatureStarter saves a file called transfer data file containing information that needs to be retrieved later in the signature process. By default, this file is stored in the system temporary folder, but another folder can be configured with PkiExpressConfig (client library settings). dispose() does not delete it, since the "complete" step needs it. The method SignatureStartResult.getTransferFile() returns the name of the file, which, along with the other fields of the result, is rendered in hidden fields on the page:

// Render the fields received form start() method as hidden fields to be used on
// the javascript or on the "complete" step.
model.addAttribute("certThumb", certThumb);
model.addAttribute("certContent", certContent);
model.addAttribute("toSignHash", result.getToSignHash());
model.addAttribute("transferFileId", result.getTransferFile());
model.addAttribute("digestAlgorithm", result.getDigestAlgorithm());
return "pades-signature-express/start";

view on source

note

Security concerns are taken in generating the name of this file, so it is impossible for a user to guess the transfer data file from another user's signing process. Also, only the filename is sent to the page, not the full path, so it is also not possible for a user to induce the web application to read a file in an arbitrary folder.

The controller returns the view pades-signature-express/start, which holds those hidden fields.

4. Realization of the digital signature algorithm in the frontend​

Once the page is loaded, the init() function of the signatureCompleteForm is invoked with references to the hidden fields:

$(document).ready(function () {
// Once the page is ready, we call the init() function on the javascript code
// (see signature-complete-form.js).
signatureCompleteForm.init({
form: $('#signForm'), // The form that should be submitted when the operation is complete.
certThumbField: $('#certThumbField'), // The "certThumb" field.
toSignHashField: $('#toSignHashField'), // The "toSignHash" field.
digestAlgorithmField: $('#digestAlgorithmField'), // The "digestAlgorithm" field.
signatureField: $('#signatureField') // The "signature" field.
});
});

view on source

We then block the page and invoke the Web PKI initialization again, but this time passing the function sign() as callback:

// ----------------------------------------------------------------------------------------------
// Initializes the signature form.
// ----------------------------------------------------------------------------------------------
function init(fe) {

// Receive form parameters received as arguments.
formElements = fe;

// Block the UI while we get things ready.
$.blockUI({ message: 'Signing ...' });

// Call the init() method on the LacunaWebPKI object, passing a callback for when the
// component is ready to be used and another to be called when an error occurs on any of the
// subsequent operations. For more information, see:
// https://docs.lacunasoftware.com/en-us/articles/web-pki/get-started.html#coding-the-first-lines
// https://webpki.lacunasoftware.com/Help/classes/LacunaWebPKI.html#method_init
pki.init({
ready: sign, // As soon as the component is ready we'll perform the signature.
defaultFail: onWebPkiError // Generic error callback defined below.
});
}

view on source

In the function sign(), we invoke the Web PKI to perform the digital signature algorithm with the private key of the certificate, using as input data the data returned by PKI Express in the backend:

// ----------------------------------------------------------------------------------------------
// Function that performs the signature on startup. At this point, the UI is already blocked.
// ----------------------------------------------------------------------------------------------
function sign() {

// Call signHash() on the Web PKI component passing the "toSignHash", the digest algorithm
// and the certificate selected by the user.
pki.signHash({
thumbprint: formElements.certThumbField.val(),
hash: formElements.toSignHashField.val(),
digestAlgorithm: formElements.digestAlgorithmField.val()
}).success(function (signature) {

// Fill the "signature" field, needed on server-side to complete the signature.
formElements.signatureField.val(signature);

// Submit the form.
formElements.form.submit();
});
}

view on source

Once the signature algorithm is finished, we fill in the result in a hidden field in the page and make a new postback to the backend:

/* ... */.success(function (signature) {

// Fill the "signature" field, needed on server-side to complete the signature.
formElements.signatureField.val(signature);

// Submit the form.
formElements.form.submit();
});

view on source

5. Signature termination on backend​

In the backend, the controller PadesSignatureExpressController is again invoked to handle the postback made from the frontend with the result of the digital signature algorithm, done for the URL /pades-signature-express/complete with a verb POST. The method postComplete() is responsible for handling the request:

@PostMapping("/complete")
public String postComplete(
@RequestParam(value = "fileId") String fileToSign,
@RequestParam String transferFileId,
@RequestParam String signature,
Model model
) throws IOException {
// ...

view on source

We get an instance of the SignatureFinisher class and provide the file being signed, the name of the transfer data file and the result of the signature algorithm done in the frontend with the Web PKI:

// Get an instance of the SignatureFinisher class, responsible for completing the
// signature process.
SignatureFinisher signatureFinisher = new SignatureFinisher();

// Set PKI default options (see Util.java)
Util.setPkiDefaults(signatureFinisher);

// Set PDF to be signed. It's the same file we used on "start" step.
signatureFinisher.setFileToSign(StorageMock.getDataPath(fileToSign));

// Set transfer file.
signatureFinisher.setTransferFilePath(transferFileId);

// Set the signature value.
signatureFinisher.setSignature(signature);

view on source

note

In the current version of the pki-express package (1.23.1), setTransferFilePath(String) is deprecated: use setTransferFileId(), which takes the same value.

We generate a new file name to receive the signed PDF:

// Generate path for output file and add to signature finisher.
String outputFile = StorageMock.generateFileId("pdf");
signatureFinisher.setOutputFilePath(StorageMock.getDataPath(outputFile));

view on source

We complete the signing process and, again, delete the temporary files that the finisher created:

// Complete the signature process.
signatureFinisher.complete();

// If you want to delete the temporary files created by this step, use the method
// dispose(). This method MUST be called after the complete() method, because it
// deletes some files needed by the method.
signatureFinisher.dispose();

view on source

After this command, the signed file is saved at the output path set above. It is then at the discretion of your application to move this file to the correct location, be it database, file system or storage service. The sample renders the view pades-signature-express/signature-info, with a link to download the signed file:

// Render the signature page (templates/pades-signature-express/signature-info.html).
model.addAttribute("signedPdf", outputFile);
return "pades-signature-express/signature-info";

view on source

View as Markdown