Firma remota

En este tutorial, se verá un ejemplo práctico de como firmar un archivo alojado en un servidor.

Para poder realizar el ejemplo necesita las siguientes herrameintas:

  • Jacobitus Total.- Esta es una herramienta desarrollada por ADSIB y que se encuentra disponible en Jacobitus Total
  • AdoptOpenJDK.- El JDK de java en su versión 11 o superior, disponible en AdoptOpenJDK
  • Tomcat.- El contenedor web Tomcat, disponible en Tomcat 9
  • NetBeans.- IDE de desarrollo para java NetBeans

Uno de los problemas al momento de firmar un documento digitalmente es tener que transferir ese documento por internet, ya que si es grande puede demorar mucho tiempo.

A contiunación se detalla un ejemplo de código que firma el documento PDF sin necesidad de transfereir el codumento.

Para poder lograr esto se requiere crear un proyecto Maven->Web Application.

A continuación podemos poner el nombre webapp y crear el proyecto.

Como se vió en el tutorial de Widgets para firmar un documento PDF necesitamos importar la librería de iText y para gestionar los servicios vamos a importar glashfish, por lo que agregamos las siguientes dependencias al archivo pom.

  1. <dependency>
  2. <groupId>com.itextpdf</groupId>
  3. <artifactId>sign</artifactId>
  4. <version>7.1.15</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.glassfish</groupId>
  8. <artifactId>javax.json</artifactId>
  9. <version>1.1.4</version>
  10. </dependency>

De igual manera, es necesario crear una clase de tipo IExternalSignature para firmar el documento, la cual la pondremos en el paquete firmador.

  1. public class ServerSignature implements IExternalSignature {
  2. protected Session session;
  3. protected byte[] answer;
  4. public ServerSignature(Session session) {
  5. this.session = session;
  6. }
  7. @Override
  8. public String getHashAlgorithm() {
  9. return DigestAlgorithms.getDigest(DigestAlgorithms.getAllowedDigest("SHA256"));
  10. }
  11. @Override
  12. public String getEncryptionAlgorithm() {
  13. return "RSA";
  14. }
  15. public void setAnswer(byte[] answer) {
  16. this.answer = answer;
  17. }
  18. @Override
  19. public synchronized byte[] sign(byte[] sh) throws GeneralSecurityException {
  20. try {
  21. JsonObjectBuilder json = Json.createObjectBuilder();
  22. json.add("method", "sign");
  23. json.add("hash", Base64.getEncoder().encodeToString(sh));
  24. try (Writer writer = session.getBasicRemote().getSendWriter()) {
  25. writer.write(json.build().toString());
  26. }
  27. wait();
  28. return answer;
  29. } catch (IOException | InterruptedException ex) {
  30. throw new GeneralSecurityException(ex.getMessage());
  31. }
  32. }
  33. }

A diferencia del anterior ejemplo lo que hacemos es solicitar la firma a través de un websocket y asignamos la respuesta del web socket mediante el método setAnswer.

Para firmar el documento vamos a crear la clase FirmadorThread ya que se requiere el uso de hilos.

  1. public class FirmadorThread extends Thread {
  2. protected ServerSignature signature;
  3. protected Session session;
  4. protected String file;
  5. protected String pem;
  6. protected Map tasks;
  7. public FirmadorThread(Session session, String file, String pem, Map tasks) {
  8. this.session = session;
  9. signature = new ServerSignature(session);
  10. this.file = file;
  11. this.pem = pem;
  12. tasks.put(file, signature);
  13. this.tasks = tasks;
  14. }
  15. @Override
  16. public void run() {
  17. try {
  18. PdfReader reader = new PdfReader(new FileInputStream("/tmp/" + file));
  19. PdfSigner signer = new PdfSigner(reader, new FileOutputStream("/tmp/prueba.signed.pdf"), new StampingProperties());
  20. Rectangle rect = new Rectangle(0, 0, 0, 0);
  21. PdfSignatureAppearance appearance = signer.getSignatureAppearance();
  22. appearance.setPageRect(rect);
  23. signer.setFieldName("sig");
  24. IExternalDigest digest = new BouncyCastleDigest();
  25. CertificateFactory cf = CertificateFactory.getInstance("X.509");
  26. X509Certificate cert = (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(pem.getBytes()));
  27. signer.signDetached(digest, signature, new Certificate[] { cert }, null, null, null, 0, PdfSigner.CryptoStandard.CADES);
  28. tasks.remove(file);
  29. } catch (IOException | GeneralSecurityException ex) {
  30. try (Writer writer = session.getBasicRemote().getSendWriter()) {
  31. writer.write("{\"label\":\"" + ex.getMessage() + "\"}");
  32. } catch (IOException ex1) {
  33. Logger.getLogger(FirmadorThread.class.getName()).log(Level.SEVERE, null, ex1);
  34. }
  35. }
  36. }
  37. }

De manera similar se firma haciendo uso de iText, sin embargo en este caso es importante hacer una gestión concurrente del archivo para que no se vaya a acceder de forma simultánea, para lo cual se agrega el archivo a un mapa y se lo remueve una vez concluído el proceso de firma.

Con esto ya tendríamos el firmador, ahora corresponde implementar el servicio y el cliente que interactuará con el Jacobitus Total, para esto crearemos la clase Documento dentro del paquete resources.

  1. @ServerEndpoint(value="/firmar")
  2. public class Documento {
  3. private final Map tasks = new TreeMap<>();
  4. @OnMessage
  5. public void mensaje(Session session, String mensaje) throws IOException, EncodeException {
  6. JsonReader jsonReader = Json.createReader(new ByteArrayInputStream(mensaje.getBytes()));
  7. JsonObject json = jsonReader.readObject();
  8. switch(json.getString("method")) {
  9. case "hash":
  10. if (json.containsKey("file")) {
  11. if (tasks.containsKey(json.getString("file"))) {
  12. try (Writer writer = session.getBasicRemote().getSendWriter()) {
  13. writer.write("{\"mensaje\":\"El archivo está siendo procesado.\"}");
  14. }
  15. } else {
  16. new FirmadorThread(session, json.getString("file"), json.getString("cert"), tasks).start();
  17. }
  18. } else {
  19. try (Writer writer = session.getBasicRemote().getSendWriter()) {
  20. writer.write("{\"mensaje\":\"No se especificó el archivo.\"}");
  21. }
  22. }
  23. break;
  24. case "sign":
  25. try (Writer writer = session.getBasicRemote().getSendWriter()) {
  26. if (tasks.containsKey(json.getString("file"))) {
  27. if (tasks.containsKey(json.getString("file"))) {
  28. ServerSignature serverSignature = tasks.get(json.getString("file"));
  29. serverSignature.setAnswer(Base64.getDecoder().decode(json.getString("sign")));
  30. synchronized(serverSignature) {
  31. serverSignature.notify();
  32. }
  33. writer.write("{\"mensaje\":\"Recibido.\"}");
  34. } else {
  35. writer.write("{\"mensaje\":\"El archivo no está en cola.\"}");
  36. }
  37. } else {
  38. writer.write("{\"mensaje\":\"No se especificó el archivo.\"}");
  39. }
  40. }
  41. break;
  42. }
  43. }
  44. }

En esencia lo que hacemos es crear un endpoint para atender las solicitudes del cliente, lo que nos permitirá obtener el hash del documento para firmarlo con el Jacobitus Total y devolver la firma para agregarla al documento.

Ahora para el cliente debemos crear la carpeta js dentro de Web Pages para programar las funciones del navegador y dentro comenzamos con el archivo token.js

  1. 'use strict';
  2. function listar(callback) {
  3. const xhttp = new XMLHttpRequest();
  4. xhttp.onload = function() {
  5. var json = JSON.parse(this.responseText);
  6. if (json.finalizado) {
  7. var options = '';
  8. const tokens = json.datos.tokens;
  9. for (let i = 0; i < tokens.length; i++) {
  10. var selected = '';
  11. if (i === 0) {
  12. selected = ' selected';
  13. }
  14. options += `${tokens[i].slot}. ${tokens[i].name}`;
  15. }
  16. callback(options);
  17. }
  18. };
  19. xhttp.open("GET", "https://localhost:9000/api/token/connected");
  20. xhttp.send();
  21. }
  22. function certificados(slot, pin, callback) {
  23. const xhttp = new XMLHttpRequest();
  24. xhttp.onload = function() {
  25. var json = JSON.parse(this.responseText);
  26. if (json.finalizado) {
  27. var certs = '';
  28. const elements = json.datos.data_token.data;
  29. for (let i = 0; i < elements.length; i++) {
  30. if (elements[i].tipo === 'X509_CERTIFICATE') {
  31. var selected = '';
  32. if (i === 0) {
  33. selected = ' selected';
  34. }
  35. certs += `${elements[i].titular.CN}`;
  36. }
  37. }
  38. callback(certs);
  39. }
  40. };
  41. xhttp.open("POST", "https://localhost:9000/api/token/data");
  42. xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
  43. xhttp.send(JSON.stringify({
  44. slot: slot,
  45. pin: pin
  46. }));
  47. }
  48. function certificado(slot, pin, label, callback) {
  49. const xhttp = new XMLHttpRequest();
  50. xhttp.onload = function() {
  51. var json = JSON.parse(this.responseText);
  52. if (json.finalizado) {
  53. const elements = json.datos.data_token.data;
  54. for (let i = 0; i < elements.length; i++) {
  55. if (elements[i].tipo === 'X509_CERTIFICATE') {
  56. if (elements[i].alias === label) {
  57. callback(elements[i].pem);
  58. break;
  59. }
  60. }
  61. }
  62. }
  63. };
  64. xhttp.open("POST", "https://localhost:9000/api/token/data");
  65. xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
  66. xhttp.send(JSON.stringify({
  67. slot: slot,
  68. pin: pin
  69. }));
  70. }
  71. function firmarHash(slot, pin, label, hash, callback) {
  72. const xhttp = new XMLHttpRequest();
  73. xhttp.onload = function() {
  74. var json = JSON.parse(this.responseText);
  75. if (json.finalizado) {
  76. callback(json.datos.firma);
  77. }
  78. };
  79. xhttp.open("POST", "https://localhost:9000/api/token/firmar_hash");
  80. xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
  81. xhttp.send(JSON.stringify({
  82. slot: slot,
  83. pin: pin,
  84. alias: label,
  85. hash: hash
  86. }));
  87. }

Para comprender mejor el código se puede consultar el tutorial Jacobitus FiDo, ya que las funciones consumen los servicios de Jacobitus Total para acceder al token criptográfico.

A continuación creamos el archivo firmar.js que será el encargado de obtener el hash del servidor, solicitar la firma al Jacobitus Total y enviar la firma al servidor para que se almacene en el documento.

  1. (function(window, document, JSON) {
  2. 'use strict';
  3. var tokens = document.getElementById('tokens');
  4. var pass = document.getElementById('pass');
  5. var certs = document.getElementById('certs');
  6. listar(function(options) {
  7. document.getElementById('tokens').innerHTML = options;
  8. });
  9. document.getElementById('actualizar').addEventListener('click', actualizar);
  10. function actualizar() {
  11. certificados(tokens.value, pass.value, function(options) {
  12. document.getElementById('certs').innerHTML = options;
  13. });
  14. }
  15. var url = 'ws://' + window.location.host + '/webapp/firmar',
  16. ws = new WebSocket(url);
  17. ws.onopen = onOpen;
  18. ws.onclose = onClose;
  19. ws.onmessage = onMessage;
  20. document.getElementById('firmar').addEventListener('click', firmar);
  21. function onOpen() {
  22. console.log('Conectado...');
  23. }
  24. function onClose() {
  25. console.log('Desconectado...');
  26. }
  27. function firmar() {
  28. console.log('Firmando...');
  29. certificado(tokens.value, pass.value, certs.value, function(cert) {
  30. ws.send(JSON.stringify({method:'hash',file:'prueba.pdf',cert:cert}));
  31. });
  32. }
  33. function onMessage(evt) {
  34. var obj = JSON.parse(evt.data);
  35. switch (obj.method) {
  36. case 'sign':
  37. firmarHash(tokens.value, pass.value, certs.value, obj.hash, function(sign) {
  38. ws.send(JSON.stringify({method:'sign',file:'prueba.pdf',sign:sign}));
  39. });
  40. break;
  41. default:
  42. alert(obj.mensaje);
  43. ws.close();
  44. break;
  45. }
  46. }
  47. })(window, document, JSON);

El código anterior se encarga de abrir una conexión websocket con el servidor y cuando el usuario presione el botón Firmar envía el certificado obtenido del token y el nombre del archivo solicitando se calcule el hash, una vez que recibe el hash lo envía al Jacobitus Total y una vez obtenida la respuesta envía la firma al servidor.

Finalmente nos queda la interfaz HTML para que el usuario pueda interactuar con el sistema.

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Firma remota</title>
  5. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  6. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  7. </head>
  8. <body class="container">
  9. <h1>Firma remota</h1>
  10. <form>
  11. <div class="mb-3">
  12. <label for="tokens" class="form-label">Token</label>
  13. <select id="tokens" class="form-select">
  14. <option>No encontrado</option>
  15. </select>
  16. </div>
  17. <div class="mb-3">
  18. <label for="pass" class="form-label">Pin</label>
  19. <div class="input-group mb-3">
  20. <input id="pass" type="password" class="form-control">
  21. <button id="actualizar" type="button" class="btn btn-primary">Actualizar</button>
  22. </div>
  23. </div>
  24. <div class="mb-3">
  25. <label for="certs" class="form-label">Certificados</label>
  26. <select id="certs" class="form-select">
  27. <option>No encontrado</option>
  28. </select>
  29. </div>
  30. <button id="firmar" type="button" class="btn btn-primary">Firmar</button>
  31. </form>
  32. <script src="js/token.js"></script>
  33. <script src="js/firmar.js"></script>
  34. </body>
  35. </html>

El documento prueba.pdf de la carpeta /tmp se firmará con el certificado seleccionado del token criptográfico.

El documento firmado será guardado con el nombre /tmp/prueba.signed.pdf

Si desea descargar el ejemplo puede hacerlo a través del siguiente link: Código de ejemplo

© ADSIB 2019 Bolivia