mercoledì 25 ottobre 2017

Inviare allegati a un web service con MTOM. (parte uno... per ora senza IBMi)

Se pensate a come allegare un grosso file in una SOAP envelope immaginerete di creare un campo BLOB codificato in base64. La codifica è utile per rispettare le specifiche (senza infilare nell'XML caratteri speciali), ma ha la brutta abitudine di diventare enorme rispetto al contenuto effettivo del file. MTOM è un meccanismo semplice e soprattutto standard per migliorare questa trasmissione e sfrutta un altro standard, XOP: in pratica, il file viene serializzato come parte della chiamata http, come se fosse un normale form html.

AGGIORNAMENTO 14/11/2017: e stata pubblicata la seconda parte qui.

(Vi apettavate una barra di sapone? Spiacente, uso quello liquido)

Di seguito vediamo come implementare un semplice sistema di archiviazione che sfrutta questa serializzazione. Il producer riceverà i file e li salverà associandoli a una determinata chiave, e sarà in grado di inviarli alla richiesta della stessa chiave, il consumer invierà e richiederà i file al servizio.

In questa prima parte il producer verrà fatto girare in locale su Tomcat, mentre il ruolo del consumer viene giocato da SoapUI che costruirà da solo il necessario per fare la chiamata e testare il tutto, nella seconda parte vedremo come implementare un consumer con RPG e di far girare tutto, compreso il producer, su IBMi.

IL PRODUCER.


Prepariamo il necessario.


Di cosa abbiamo bisogno:

  • Java JDK 6 (bisogna registrarsi);
  • Eclipse Oxygen for JEE Developer (ho utilizzato la versione 4.7.1a);
  • Maven (ho utilizzato la versione 3.2.5);
  • Tomcat (ho utilizzato la versione 8.5), per provare il progetto in locale;

NB: le versioni delle librerie utilizzate sono compatibili con Java 6, questo per poter poi installare il tutto sulla versione 8.1 del server delle applicazioni integrato dell' IBMi, come vedremo in seguito.

Installate tutto quanto, in particolare Maven e Tomcat andranno estratti ognuno in una sua cartella alle quali, per comodità, mi riferirò come $MAVEN_HOME e $TOMCAT_HOME, mentre la cartella di installazione di Java sarà $JAVA_HOME.
Per prima cosa impostiamo Eclipse:

  • per Java: dal menu Windows > Java > Installed JREs > Add selezioniamo "Standard VM" e andiamo avanti, poi "Directory..." e andiamo a cercare $JAVA_HOME, alla fine impostiamo come JRE di default;
  • per Maven: dal menu Windows > Preferences > Maven > Installations > Add selezioniamo "External" e poi "Directory..." e andiamo a cercare $MAVEN_HOME;
  • per Tomcat: dal menu Windows > Preferences > Server > Runtime Environments > Add  selezioniamo "Apache Tomcat 8.5" e andiamo avanti, quindi "Browse..." e andiamo a cercare $TOMCAT_HOME, viene proposto di lavorare con la JRE di default definita prima.

Ok, procediamo. Creiamo un nuovo progetto Maven, dal menu File > New > Maven Project, chiediamo di non utilizzare un archetipo e andiamo avanti in questo modo:


Diamo almeno un nome gruppo e un nome artefatto al nostro progetto, il packaging da utilizzare invece è war:


e finalizziamo.

Come prima cosa Eclipse ci avverte che nel nuovo progetto manca il descrittore web.xml, per cui creiamone uno cliccando con il tasto destro sul progetto e quindi Java EE Tools > Generate Deployment Descriptor Stub.

Come seconda cosa dobbiamo inserire un po' di dipendenze e plugin nel progetto, fortuna che Maven si occupa di reperire tutte le librerie necessarie! A patto ovviamente di farglielo sapere indicandolo nel suo descrittore, apriamo quindi il file pom.xml e inseriamo questo dentro al tag <project>:
<build>
 <pluginManagement>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.6.1</version>
    <configuration>
     <source>1.6</source>
     <target>1.6</target>
    </configuration>
   </plugin>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
     <warSourceDirectory>src/main/webapp</warSourceDirectory>
     <webXml>src/main/webapp/WEB-INF/web.xml</webXml>
     <warName>MTOMService</warName>
    </configuration>
   </plugin>
  </plugins>
 </pluginManagement>
</build>
In questo modo aggiungiamo qualche plugin necessario a compilare il progetto, istruito per creare un file di nome MTOMService.war .

Sempre nello stesso tag <project> aggiungiamo anche qualche dipendenza:
<dependencies>
 <dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-frontend-jaxws</artifactId>
  <version>2.5.2</version>
 </dependency>
 <dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-transports-http</artifactId>
  <version>2.5.2</version>
 </dependency>
 <dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-core</artifactId>
  <version>3.0.6.RELEASE</version>
 </dependency>
 <dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>3.0.6.RELEASE</version>
 </dependency>
 <dependency>
  <groupId>org.apache.servicemix.bundles</groupId>
  <artifactId>org.apache.servicemix.bundles.saaj-impl</artifactId>
  <version>1.3.18_1</version>
 </dependency>
</dependencies>
Useremo Spring Framework per instanziare una servlet CXF, inoltre introduciamo una dipendenza a SAAJ implementato da Apache, in quanto l'implementazione di IBM integrata con il server delle applicazioni non è completamente compatibile con CXF.

NB: le versioni utilizzate sopra sono sempre quelle compatibili con Java 6, il motivo è sempre lo stesso.

Ultime configurazioni e poi implementiamo, giuro, per ora apriamo il Deployment Descriptor:


e sostituiamolo completamente in questo modo:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
  <display-name>MTOMService</display-name>
  <servlet>
    <servlet-name>cxfservlet</servlet-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>cxfservlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>
</web-app>

Implementiamo il sistema.


Mettiamo le mani in pasta, finalmente, essendo questo un esempio molto semplice, abbiamo bisogno di scrivere solo tre classi e per comodità mettiamole quindi sotto un unico package che io ho chiamato it.zenovalle.examples.

Per prima creiamo una semplice classe di proprietà (POJO) che descrive il documento che verrà scambiato, oltre al contenuto del documento stesso.

ArchiveRequest.java
package it.zenovalle.examples;

import javax.activation.DataHandler;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlMimeType;
import javax.xml.bind.annotation.XmlType;

@XmlType
@XmlAccessorType(XmlAccessType.FIELD)
public class ArchiveRequest {
 
 @XmlMimeType("application/octet-stream")
 protected DataHandler fileLoad;
 protected String fileName;
 protected String fileType;
 protected String key;
 protected String description;
 
 public DataHandler getFileLoad() {
  return fileLoad;
 }
 
 public void setFileLoad(DataHandler fileLoad) {
  this.fileLoad = fileLoad;
 }
 
 public String getFileName() {
  return fileName;
 }
 
 public void setFileName(String fileName) {
  this.fileName = fileName;
 }
 
 public String getFileType() {
  return fileType;
 }
 
 public void setFileType(String fileType) {
  this.fileType = fileType;
 }
 
 public String getKey() {
  return key;
 }
 
 public void setKey(String key) {
  this.key = key;
 }
 
 public String getDescription() {
  return description;
 }
 
 public void setDescription(String description) {
  this.description = description;
 }
 
}
Vi faccio solo notare che:
  • l'intera classe deve essere annotata come XmlType e con tipo di accesso FIELD;
  • la proprietà che contiene i dati del file è di tipo DataHandler invece di byte[], questo ci permette di trattare meglio i dati, mantenendo il meccanismo di conversione automatico;
  • la conversione automatica viene effettuata marchiando la proprietà con un'annotazione del tipo @XmlMimeType("application/octet-stream") che spiega in che formato attendersi i dati (binario, per l'appunto).
Ora scriviamo l'interfaccia con cui si presenta il Web Service.

ArchiveServer.java
package it.zenovalle.examples;

import javax.activation.DataHandler;
import javax.jws.WebParam;
import javax.jws.WebService;

@WebService
public interface ArchiveServer {

 public String archiveFile(@WebParam(name="request") ArchiveRequest request);
 
 public DataHandler getFile(@WebParam(name="key") String key);

}
Anche in questo caso utilizziamo le annotazioni per far capire al sistema che l'interfaccia è un @WebService e anche per rinominare i parametri dei vari metodi con @WebParam(name="***").

Infine implementiamo l'interfaccia con una classe vera e propria.

ArchiveServerImpl.java
package it.zenovalle.examples;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.jws.WebService;
import javax.xml.bind.annotation.XmlMimeType;
import javax.xml.ws.WebServiceException;

@WebService(endpointInterface = "it.zenovalle.examples.ArchiveServer",
            serviceName = "ArchiveServer")
public class ArchiveServerImpl implements ArchiveServer{

 Map<String, File> files = new LinkedHashMap<String, File>();
 private File tmpdir = new File(System.getProperty("java.io.tmpdir"),"uploaded");

 @Override
 public String archiveFile(ArchiveRequest request) {

  tmpdir.mkdir();

  if(request==null){
   throw new WebServiceException("Upload Failed");
  }

  File file = new File(tmpdir, request.getFileName());

  try {
   InputStream is = request.getFileLoad().getInputStream();
   OutputStream os = new FileOutputStream(file);

   file.createNewFile();

   byte[] b = new byte[100000];
   int bytesRead = 0;
   while ((bytesRead = is.read(b)) != -1) {
    os.write(b, 0, bytesRead);
   }
   
   is.close();
   os.flush();
   os.close();

   files.put(request.getKey(), file);
   
   System.out.println("File "+ request.getFileType() +" archived in " + file.getPath() + " - " +request.getKey() +" "+request.getDescription());

  } catch (IOException e) {
   e.printStackTrace();
   return "Upload Failed";
  }

  return "Upload Complete";
 }

 @Override
 public
 @XmlMimeType("application/octet-stream") DataHandler getFile(String key) {

  File file = files.get(key);
  DataSource dataSource = new FileDataSource(file);
  System.out.println("Returning " + key + " : "+ file.getPath());
  return new DataHandler(dataSource);
 }
}
Ancora l'annotazione @WebService, questa volta però con alcune informazioni in più su quale interfaccia viene implementata (che viene utilizzata come endpoint, definisce quindi il contratto WSDL) e su come si chiama il servizio. Viene anche annotato il tipo di ritorno DataHandler del metodo getFile in maniera che possa essere serializzato anch'esso con MTOM.

L'implementazione riceve il DataHandler e ne ricava l'InputStream da cui leggere i dati che sono poi scritti in un File creato nella sotto-cartella uploaded della cartella temporanea di sistema. La chiave viene associata alla posizione tramite una Map che risiede in memoria, se viene richiesto il download il web service interroga la Map per sapere che File aprire.

Ok, manca solo di chiedere a Spring di farci il favore di instanziare la servlet, creiamo il file cxf-servlet.xml sotto src > main > webapp > WEB-INF descritto in questo modo:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxws="http://cxf.apache.org/jaxws"
 xmlns:cxf="http://cxf.apache.org/core" xmlns:soap="http://cxf.apache.org/bindings/soap"
 xsi:schemaLocation="http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

 <jaxws:server id="archiveServer"
  serviceClass="it.zenovalle.examples.ArchiveServer" address="/archiveServer">
  <jaxws:serviceBean>
   <bean class="it.zenovalle.examples.ArchiveServerImpl" />
  </jaxws:serviceBean>
  <jaxws:properties>
   <entry key="mtom-enabled" value="true" />
   <entry key="attachment-directory" value="/tmp/"/>
   <entry key="attachment-memory-threshold" value="4000000"/>
  </jaxws:properties>
 </jaxws:server>

</beans>
La proprietà più importante è mtom-enabled che deve essere messa a true per far funzionare tutto il meccanismo, il resto delle proprietà è per indicare di trattare in memoria gli allegati fino a 4 MB, altrimenti li salva prima nella cartella temporanea.

Ora abbiamo veramente tutto, quindi...

Compiliamo e avviamo


Con il tasto destro del mouse sul nostro progetto cerchiamo Run As... e poi Maven install, se tutto ok Maven assemblerà il file MTOMService.war nella cartella target.


Bene, ora facciamo partire tutto in locale, sempre tasto destro sul progetto, sempre Run As... , questa volta scegliamo Run on Server e scegliamo il runtime Tomcat configurato prima. Comparirà il browser interno a Eclipse che cercherà di interrogare la servlet, inizialmente con scarso successo:


E sufficiente aggiungere il suffisso services all'URL per ottenere un risultato migliore:

La lista dei servizi offerti dalla servlet.

In particolare il link che la pagina ci invita a cliccare (cliccate!) non è nient'altro che il WSDL messo a disposizione dal Web Service: copiate questo link negli appunti, ci sarà utile a breve.

IL CONSUMER.


Di cosa abbiamo bisogno:
  • SoapUI (ho usato la versione 5.3.0).

SoapUI è un benchmark molto ben fatto ed OpenSource che ci permette di testare i nostri Web Service, può essere usato sia per SOAP che per REST, e permette anche di inviare allegati con MTOM (con qualche accorgimento, come vedremo). Può ricoprire benissimo il ruolo del nostro consumer in modo da avere finalmente la soddisfazione di vedere funzionare il tutto.

Una volta installato apriamolo e chiediamo subito di creare un nuovo progetto SOAP, con il tastone posizionato in alto, diamo un nome al progetto e incolliamo il link al WSDL copiato prima:


E diamo l'ok, SoapUI penserà ad analizzare il WSDL e creerà l'ambiente necessario per testare i vari metodi messi a disposizione del web service.

Espandiamo il nodo corrispondente al metodo archiveFile, il programma ha già creato una Request di default che possiamo rimaneggiare per fare un test. Normalmente la schermata è separata in due, in una parte possiamo modificare la Request, nell'altra vedremo la risposta data dal web service, per prima cosa abilitiamo MTOM nelle proprietà della Request:

Enable MTOM = true

Poi inseriamo dei valori di prova nei vari tag della SOAP Envelope, tranne per ora fileLoad. Per inserire un allegato facciamo click sul tab Attachments sotto la Request e poi sul tasto + :

(Nell'esempio ipotizzo che venga scelto un file pdf)

NB: il programma ci chiede di inserire in cache il documento, vi direi di rispondere di sì, in questo modo viene inglobato nel progetto SoapUI che poi potrete salvare.

Ora modifichiamo il tag fileLoad inserendo il nome del file scelto, ma preservando il prefisso cid: (nel mio esempio: cid:testfile.pdf ), torniamo in Attachments, scegliamo Part e vediamo che il nostro cid viene elencato tra quelli assegnabili all'allegato:

Il Type viene modificato automaticamente in XOP, proprio quello che ci serve per MTOM.

Ora è tutto pronto, clicchiamo sulla freccettina verde sopra alla Request per far partire la richiesta e, se ok, nella Response dovrebbe comparire "Upload Complete":

Sento scatenarsi le endorfine...

Spostiamoci sulla console di Eclipse, sul quale attendeva silenzioso Tomcat, e dovremmo trovare un output generato dal nostro servizio:

File application/pdf archived in C:\Users\<user>\AppData\Local\Temp\uploaded\testfile.pdf - 1234567890 This is a test file

Che sono proprio i dati che sono stati passati! Come controprova, interroghiamo la posizione e cerchiamo il file, ed eccolo qui:


Rimane solo da verificare il download! Come prima espandiamo getFile, abilitiamo MTOM e modifichiamo il tag key con la stessa chiave inserita prima (nell'esempio: 1234567890) e avviamo:


Troviamo sempre l'allegato in Attachments, ma questa volta sotto la Response, con doppio click possiamo esaminarlo: è proprio lui e come maggior conferma che sia quello caricato prima, vediamo la console di Eclipse:

Returning 1234567890 : C:\Users\<user>\AppData\Local\Temp\uploaded\testfile.pdf

Questo è tutto, per semplicità ho inserito un file di salvataggio del progetto SoapUI (MTOMService-soapui-project.xml) nel repository apposito che contiene tutti i sorgenti mostrati nel post, nel caso potete importare tutto come progetto Eclipse usando EGit.

Nella prossima parte vedremo come prendere tutto e portarlo su IBMi.

PS: dopo tutta questa spiegazione, non vi è rimasta la curiosità di capire com'è fatta in fin dei conti una trasmissione MTOM? Potete vederlo facilmente da SoapUI, nella Request di archiveFile, dopo aver fatto partire la richiesta e cliccando sul tag Raw poco sotto:

No BLOB, No Base64, baby.


Aggiungo qualche link per approfondire l'argomento:

Aggiornamento 25/10/2017 h 22:35 : Aggiunta bibliografia.

lunedì 2 ottobre 2017

Tu (in batch) non puoi passare! O forse sì...

SFTP è un protocollo per lo scambio file in maniera sicura, in quanto usa ssh come layer di trasporto di dati crittografati, è molto, molto più sicuro di un normale FTP e viene quindi spesso richiesto come metodo di scambio file. Sul nostro IBM i è facilmente utilizzabile a patto di avere installato i programmi su licenza 5733SC1 opzione *BASE - IBM Portable Utilities for i 5733SC1 opzione 1 - OpenSSH, OpenSSL, zlib e chiaramente il PASE (qui su come verificare).

Una volta installate le opzioni collegarsi a un server sftp è semplice, poniamo ad esempio di doverci collegare a host.ext, e ci sono stati dati utente e password:
CALL QP2TERM
sftp utente@host.ext
al primo collegamento con host.ext verrà richiesto se inserire l'indirizzo tra quelli affidabili a cui collegarsi, per cui digitate yes e poi verrà richiesta la password, quindi ora potete inserire i comandi per lo scambio di file.

Ok... ma se dovete farlo in batch? Il comando sftp permette di lanciare dei comandi contenuti in uno script apposito... abbiamo ad esempio il nostro file scambio.txt salvato sulla root dell'IFS e deve essere inviato sulla cartella remota /scambiofile, potremmo scrivere la lista dei comandi necessari e salvarla in un altro file chiamato script.txt (per comodità salvato sempre sulla root), per cui:
EDTF STMF('/script.txt')
e inseriamo
cd /scambiofile
put /scambio.txt
il comando quindi diventa:
sftp -b/script.txt utente@host.ext
questa volta i comandi vengono eseguiti in automatico, il problema è che chiede comunque la password! Chiedere a qualcuno di effettuare l'autenticazione ogni volta potrebbe non essere una soluzione praticabile, l'ideale è che il processo parta completamente in automatico e non interattivo, di seguito illustro un paio di soluzioni, partendo dalla più sicura.

AUTENTICAZIONE TRAMITE CERTIFICATI


Si tratta di creare una coppia di chiavi di crittografia, una privata e una pubblica, i certificati generati sono personali e vi identificano in maniera univoca, per cui possono essere usati per l'autenticazione.
Dovete consegnare la parte pubblica al gestore del servizio SFTP affinché l'aggiunga alla lista delle chiavi accettate, sempre che sia possibile utilizzare questa tipologia da parte loro.

Prima di tutto dovreste collegarvi con l'utente che intendete usare per effettuare il collegamento batch, e se non l'avete già fatto collegatevi manualmente a host.ext con un comando sftp o ssh, lo scopo è di salvare il certificato del sito tra quelli riconosciuti e creare i certificati nella home dell'utente corretto.

Preferisco le mie chiavi come preferisco il caffè: lunghe, per crearle digitiamo a terminale:
CALL QP2TERM

ssh-keygen -b 4096
(crea una chiave lunga 4096, lo standard è invece 2048)

Accettate il nome di default delle chiavi (id_rsa per la chiave privata, id_rsa.pub per la chiave pubblica) e non inserite nessuna passphrase.

A questo punto dovreste consegnare al provider il file con la chiave pubblica id_rsa.pub affinché lo importino nel loro server, oppure potreste provare a farlo da soli sfruttando lo stesso collegamento ssh:
CALL QP2TERM

scp ~/.ssh/id_rsa.pub utente@host.ext:~/.ssh/authorized_keys
ed inserite la password, per questa volta.
Se ok, se provate a ricollegarvi via sftp o ssh, non dovrebbe chiedervi password ma dirvi che è stato accettato il vostro certificato ("Authenticating with public key"), ora non vi resta che mettere in piedi il vostro servizio di scambio in batch senza che resti bloccato in attesa di interattività.


INSTALLARE SSHPASS


Sshpass è una utility che simula il comportamento dell'utente e inserisce la password quando richiesta e, soprattutto, in batch. Andrà compilato e installato da sorgente, per cui per prima cosa verificate di avere il necessario per compilare sorgenti C - sto parlando di gcc e di make - altrimenti vi rimando alla mia guida pubblicata qui, in particolare fate attenzione ad inserire le variabili d'ambiente PASE_PATH e PATH: saranno utili dopo per trovare il comando sshpass.

Innanzitutto scarichiamo l'ultima release dei sorgenti compattati con in formato tar, al momento in cui scrivo è la 1.06; se avete seguito la guida citata prima dovreste avere la cartella /QOpensys/download e visto che mi piace l'ordine creiamo qui una sotto cartella dove tenere il file tar e una cartella dove inserire i file scompattati:
CALL QP2TERM

mkdir /Qopensys/download/tar

mkdir /QOpensys/opt/sshpass
Possiamo scaricare il file da pc e poi caricarlo via ftp su /Qopensys/download/tar e poi da terminale:
cd /Qopensys/download/tar

gtar -xvf sshpass-1.06.tar.gz -C /QOpensys/opt/sshpass
Spostiamoci sulla cartella appena scompattata e iniziamo il primo passo per lo compilazione, chi è pratico di sistemi unix/linux riconoscerà la famosa triade (./configure, make e make install), ma per compilare su IBM i sono necessarie alcune variazioni:
cd /QOpensys/opt/sshpass/sshpass-1.06

./configure
Completato il passo di configurazione viene creato il file config.h, questo file definisce un alias rpl_malloc che però su IBM i non trova, dobbiamo toglierlo manualmente per cui (da terminale normale):
EDTF STMF('/QOpenSys/opt/sshpass/sshpass-1.06/config.h')
trovate la riga:
#define malloc rpl_malloc
e commentatela:
/* #define malloc rpl_malloc */
ora possiamo proseguire:
gmake

gmake install
se digitate il comando sshpass dovrebbe comparire l'help:

(tutto ok)

Non resta che mettere in piedi il servizio batch, solo che a questo punto il comando per accedere via sftp diventa:
sshpass -p "password" sftp -oBatchMode=no -b/script.txt utente@host.ext

NB: sshpass vi costringe ad avere la password scritta da qualche parte, ed è quindi la soluzione giudicata come meno sicura. Dovrebbe essere utilizzata come ultima spiaggia in caso di impossibilità ad utilizzare i certificati.

(Mia reazione all'affermazione: "non siamo in grado di utilizzare le chiavi" ... successo veramente...)