Pular para o conteúdo principal

Como funciona a assinatura com PKI Express em Java

Este artigo explica como funciona o processo de assinatura digital realizado no projeto de exemplos do PKI Express em Java.

Principais arquivos de código​

Processo de assinatura​

1. Usuário vai para a página de assinatura​

Nesse momento, o usuário visita a URL /pades-signature-express?fileId={fileToSign} (GET). O controller PadesSignatureExpressController é o responsável por atender à requisição, mais especificamente no método 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

Nesse momento, apenas verificamos que o arquivo a ser assinado existe e renderizamos a página de assinatura, a view pades-signature-express/index.

observação

O parâmetro fileId e a classe StorageMock são meramente detalhes do exemplo: representam a forma como a sua aplicação armazena os arquivos.

Ao final do carregamento da página, passamos referências aos elementos da página para o módulo de JavaScript 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

O módulo signatureStartForm bloqueia a tela e dispara a inicialização do Web PKI, passando a função loadCertificates() como callback para quando a inicialização tiver sido concluída, e a função onWebPkiError() como defaultFail, o callback de erro de qualquer operação posterior do Web PKI:

// ----------------------------------------------------------------------------------------------
// 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

Na função loadCertificates(), disparamos o carregamento dos certificados disponíveis no computador do usuário, populando o elemento <select> da página:

// ----------------------------------------------------------------------------------------------
// 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. Usuário clica no botão Sign File​

Anteriormente, o módulo de JavaScript signatureStartForm registrou a função de JavaScript startSignature() para ser chamada quando o botão Sign File é clicado:

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

view on source

observação

Note que é fundamental que o botão na página tenha type="button", e não type="submit", caso contrário ocorreria um postback imediatamente ao clicar no botão.

Portanto, quando o usuário clica no botão Sign File, a função startSignature() do signatureStartForm é invocada:

// ----------------------------------------------------------------------------------------------
// 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

Nessa função, após bloquear a página, obtemos o thumbprint do certificado selecionado, que será necessário em outro momento do processo de assinatura. Invocamos então a função readCertificate() do Web PKI para obter a codificação do certificado escolhido (parte pública do certificado, o ".cer", sem a chave privada):

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

view on source

Uma vez lido o certificado, preenchemos os campos ocultos da página com o thumbprint e o certificado e fazemos o 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. Início da assinatura no backend​

No backend, o controller PadesSignatureExpressController novamente é invocado para tratar o postback feito a partir do frontend com o certificado escolhido para realizar a assinatura, feito para a URL /pades-signature-express/start com verbo POST. O método postStart() é responsável por tratar a requisição:

/**
* 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

Nesse ponto, iniciamos a assinatura usando a classe PadesSignatureStarter, passando a política de assinatura, o arquivo a ser assinado e a codificação do certificado escolhido para assinatura:

// 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

A classe é criada sem argumentos e, por isso, usa o PkiExpressConfig padrão: o PKI Express na pasta de instalação padrão e a pasta temporária do sistema (veja configurações da biblioteca cliente). O Util.setPkiDefaults() aplica o que todas as operações do exemplo têm em comum: as raízes confiáveis, incluindo a raiz de teste da Lacuna quando configurada (confiando em certificados que não sejam da ICP-Brasil), o modo offline e a autoridade de carimbo de tempo.

Em seguida, configuramos a aparência da assinatura:

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

view on source

observação

Para mais detalhes sobre as opções de configuração da aparência da assinatura, veja a classe PadesVisualElements e o artigo representação visual de assinaturas PAdES.

Por fim, concluímos a etapa inicial, obtendo os dados necessários para realizar o algoritmo de assinatura digital, e apagamos os arquivos temporários que o starter criou:

// 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

Um ponto importante: a classe PadesSignatureStarter salva um arquivo chamado transfer data file contendo informações que precisam ser recuperadas mais à frente no processo de assinatura. Por padrão, esse arquivo é armazenado na pasta temporária do sistema, porém outra pasta pode ser configurada com o PkiExpressConfig (configurações da biblioteca cliente). O dispose() não o apaga, já que a etapa de finalização precisa dele. O método SignatureStartResult.getTransferFile() retorna o nome do arquivo, o qual, junto com os demais campos do resultado, é preenchido em campos ocultos na página:

// 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

observação

Preocupações de segurança são tomadas na geração do nome desse arquivo, de modo que é impossível um usuário adivinhar o transfer data file de um processo de assinatura de outro usuário. Além disso, apenas o nome do arquivo é enviado para a página, não o caminho completo, de modo que também não é possível a um usuário induzir a aplicação web a ler um arquivo em uma pasta arbitrária.

O controller retorna a view pades-signature-express/start, que contém esses campos ocultos.

4. Realização do algoritmo de assinatura digital no frontend​

Uma vez carregada a página, a função init() do signatureCompleteForm é invocada com referências aos campos ocultos:

$(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

Bloqueamos então a página e invocamos novamente a inicialização do Web PKI, porém dessa vez passando a função sign() como 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

Na função sign(), invocamos o Web PKI para realizar o algoritmo de assinatura digital com a chave privada do certificado, usando como dados de entrada os dados retornados pelo PKI Express no 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

Uma vez finalizado o algoritmo de assinatura, preenchemos o resultado em um campo oculto na página e fazemos um novo postback para o 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. Finalização da assinatura no backend​

No backend, o controller PadesSignatureExpressController novamente é invocado para tratar o postback feito a partir do frontend com o resultado do algoritmo de assinatura digital, feito para a URL /pades-signature-express/complete com verbo POST. O método postComplete() é responsável por tratar a requisição:

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

view on source

Obtemos uma instância da classe SignatureFinisher e fornecemos o arquivo sendo assinado, o nome do transfer data file e o resultado do algoritmo de assinatura feito no frontend com o 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

observação

Na versão atual do pacote pki-express (1.23.1), o setTransferFilePath(String) está obsoleto: use o setTransferFileId(), que recebe o mesmo valor.

Geramos um novo nome de arquivo para receber o PDF assinado:

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

view on source

Finalizamos o processo de assinatura e, novamente, apagamos os arquivos temporários que o finisher criou:

// 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

Após esse comando, o arquivo assinado encontra-se salvo no caminho de saída definido acima. Fica, então, a critério da sua aplicação mover esse arquivo para o local correto, seja em banco de dados, sistema de arquivos ou serviço de storage. O exemplo renderiza a view pades-signature-express/signature-info, com um link para baixar o arquivo assinado:

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

view on source

Ver em Markdown