(Italiano) Liferay 7.2: Esempio di Two-Way SSL/TLS Mutual Authentication Client
Quanti di voi hanno dovuto affrontare i problemi legati alla realizzazione di un client HTTPS verso un servizio in Mutua Autenticazione? E quanti di voi in un contesto Liferay? Posso immaginare che il numero sia alto, per non contare le notti passate a fare debugging.
In questo articolo vorrei indicarvi tutti gli step necessari per portare a casa il risultato senza alcuna fatica e notti insonne. Lo scenario presentato è calato su Liferay, ricordo però che i concetti espressi sono di carattere generale.
La versione Liferay di riferimento per questo articolo è la 7.2.1 GA2 in formato Bundle Tomcat. Affinché possiate seguire per intero gli step mostrati nel corso dell'articolo, è necessario che sulla vostra macchina sia installato Docker (versione 17.x, 18.x o 19.x).
Nel video Liferay 7.2 - Gogo Shell Command for the Two-Way SSL/TLS Mutual Authentication mostro come eseguire l'esempio del progetto GitHub gogo-shell-tls-mutual-sample in semplici passaggi usando Docker e Docker Compose.
1. Two-Way SSL/TLS Mutual Authentication
La mutua autenticazione basata sul protocollo SSL/TLS (Secure Sockets Layer e il successore Transport Layer Security) si riferisce a due parti che si autenticano reciprocamente attraverso la verifica del certificato digitale fornito in modo che entrambe i partecipanti siano sicuri dell'identità altrui.
Il processo di autenticazione e creazione di un canale crittografato utilizzando il meccanismo di autenticazione reciproca (o mutua autenticazione o Two-Way SSL/TLS Mutual Authentication) basata su certificati prevede i passaggi seguenti:
- Un client richiede l'accesso a una risorsa protetta;
- Il server presenta il suo certificato al client;
- Il client verifica il certificato del server;
- Se ha successo, il client invia il suo certificato al server;
- Il server verifica le credenziali del cliente;
- In caso di esito positivo, il server concede l'accesso alla risorsa protetta richiesta dal client.
In Figura 1 è mostrato quello che accade durante il processo di autenticazione reciproca (o mutua autenticazione).
Nel caso in cui vogliate approfondire l'argomento su questo meccanismo di autenticazione, posso consigliare la lettura dell'articolo An Introduction to Mutual SSL Authentication pubblicato su CodeProject da Elvin Cheng.
2. Presentazione dello scenario e obiettivo
Supponiamo che dal nostro portale Liferay abbiamo la necessità di accedere ad uno o più servizi esposti in HTTPS e protetti da una Two-Way SSL/TLS Mutual Authentication. In questo tipo di scenario (mostrato in figura 2) come possiamo implementare il client per accedere al servizio? Quali sono inoltre i prerequisiti?
Liferay facilità di gran lunga il lavoro d'implementazione del client HTTPS, in virtù del fatto che la piattaforma mette a disposizione la classe HttpUtil (esistente dalle versioni 6.x di Liferay) che si sposa in pieno con le nostre necessità e che implementa l'interfaccia com.liferay.portal.kernel.util.Http.
Affinché sia possibile accedere al servizio è indispensabile soddisfare una serie di prerequisiti e applicare un set di configurazioni al portale. A seguire il dettaglio dei prerequisiti e configurazioni.
- Devono essere aperti i flussi tra il portale Liferay e il servizio HTTPS a cui dobbiamo accedere. Può sembrare ovvio ma preferisco ricordarlo;
- Dobbiamo avere il certificato digitale solitamente rilasciato dal proprietario del servizio o dalla PKI di competenza. Senza di esso non sarà possibile accedere al servizio. Il formato del certificato può essere solitamente scelto tra: JKS o PKCS#12;
- Dobbiamo eventualmente ottenere il bundle della Certificate Chains qualora sia necessario e inserirlo nel trustStore utilizzato dal nostro portale Liferay. Per impostazione predefinita, il trustStore letto è quello della JVM con cui il portale è eseguito (location per macOS: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/security/cacerts);
- Dobbiamo configurare l'application server (Tomcat, WildFly, JBOSS EAP, etc.) affinché sia impostato il keyStore ed eventualmente anche il trustStore.
Prima di passare all'implementazione del client HTTPS, occorrerà quindi soddisfare i punti indicati in precedenza. Nel prossimo paragrafo affronteremo la configurazione del portale Liferay.
3. Dettagli sul servizio HTTPS
L'ambientazione descritta nel capitolo 2. Presentazione dello scenario e obiettivo e mostrata in figura 2, menziona l'accesso ai servizi HTTPS protetti da una Two-Way SSL/TLS Mutual Authentication. Dato l'aspetto pratico di questo articolo, mi son preoccupato di mettere a vostra disposizione un'ambiente per eseguire il test dell’implementazione del client SSL/TLS in Mutua Autenticazione.
Più di un anno fa ho realizzato il progetto Apache HTTP 2.4: How to Build a Docker Image for SSL/TLS Mutual Authentication il cui codice sorgente è disponibile sul repository GitHub docker-apache-ssl-tls-mutual-authentication e l'immagine Docker su Docker Hub amusarra/apache-ssl-tls-mutual-authentication.
L'idea è quella di sfruttare quest'immagine Docker per avere a disposizione dei servizi HTTPS protetti dal meccanismo di Mutua Autenticazione. Dalla versione 1.2.0 di questo progetto è stato introdotto httpbin, quest'ultimo realizzato da Kenneth Reitz.
A seguire i dettagli del servizio Apache SSL/TLS Mutual Authentication:
- Base URL HTTPS Services: https://tls-auth.dontesta.it/secure/api
- Server Certificate:
- CN: tls-auth.dontesta.it
- Issuer: Let's Encrypt Authority X3
- Client Certificate:
- Subject: EMAILADDRESS=antonio.musarra@gmail.com, CN=antonio.musarra@gmail.com, OU=IT Labs, O=Antonio Musarra Digital Personal Certificate, ST=Italy, L=Bronte, C=IT
- Issuer: EMAILADDRESS=soc@dontesta.it, CN=Antonio Musarra's Blog Certification Authority, OU=IT Security Department, O=Antonio Musarra's Blog, ST=Italy, L=Rome, C=IT
La figura a seguire mostra alcuni dettagli del Server Certificate rilasciato da Let's Encrypt Authority X3 ed esposto dal servizio HTTPS che risponde all'FQDN tls-auth.dontesta.it.
Il certificato client ci permetterà di accedere ai servizi HTTPS protetti. Questo certificato (per ovvie ragioni di test) è pubblicamente disponibile e in formato JKS e PKCS#12. Teniamo da parte il certificato in formato JKS che utilizzeremo successivamente nella configurazione del portale Liferay.
La figura a seguire mostra alcuni dettagli del Client Certificate rilasciato dalla CA (di pura invenzione) Antonio Musarra's Blog Certification Authority.
Dopo questa breve introduzione sul servizio HTTPS che utilizzeremo come test per il nostro client in esecuzione su Liferay, non dobbiamo fare altro che lanciare il container Docker docker-apache-ssl-tls-mutual-authentication utilizzando il comando a seguire.
Il servizio sarà disponibile sulla porta (standard SSL/TLS) 443, che deve essere libera, in caso contrario potreste utilizzare un numero di porta libero (esempio: 9443, 8443, etc.).
docker run -i -t -d -p 443:10443 --name=tls-auth amusarra/apache-ssl-tls-mutual-authentication
Se non ci sono stati errori di esecuzione del comando docker, da questo momento sarà possibile raggiungere il servizio di mutua autenticazione SSL/TLS utilizzando il browser. Per evitare il comune errore SSL_ERROR_BAD_CERT_DOMAIN da parte del browser accedendo al servizio tramite la URL https://127.0.0.1, bisogna aggiungere al proprio file di hosts la riga a seguire.
## # Servizio di mutua autenticazione via Apache 2.4 HTTP ## 127.0.0.1 tls-auth.dontesta.it
Adesso che il servizio HTTPS è attivo possiamo procedere con il successivo step di configurazione del portale Liferay.
4. Configurazione del portale Liferay
Le specifiche Java Secure Socket Extension (JSSE) prevedono la personalizzazione di diversi aspetti attraverso l'impostazione di specifiche system properties e security properties. La sezione Customizing JSSE riassume quali di questi aspetti possono essere personalizzati, quali sono le impostazioni predefinite e quali meccanismi sono utilizzati per fornire la personalizzazione.
Nel nostro caso le system properties che dobbiamo impostare sono le seguenti:
- javax.net.ssl.keyStore: Indicare il path assoluto del keyStore (esempio: /home/amusarra/client-keystore.jks);
- javax.net.ssl.keyStorePassword: Indicare la password di accesso al keyStore;
- javax.net.ssl.trustStore: Indicare il path assoluto del trustStore (esempio: /home/amusarra/client-truststore.jks);
- javax.net.ssl.keyStorePassword: Indicare la password del trustStore.
In ambienti di produzione è vivamente sconsigliato specificare in chiaro le password del keyStore e trustStore ma utilizzare un meccanismo che metta al sicuro le password.
Ogni application server, OOTB (out-of-the-box) offre il proprio meccanismo di sicurezza. Prendiamo il caso di Red Hat JBoss Enterprise Application Platform che prevede il Credential Store e Password Vault. Fate riferimento alla documentazione ufficiale del vostro application server utilizzato su cui è installato il portale Liferay.
Esistono altre due proprietà che in alcuni casi possono essere utilizzate per impostare il tipo di keyStore. Il tipo di keyStore di default è JKS, potrebbero presentarsi dei casi per cui sia necessario impostare un tipo di keyStore diverso. La sezione Additional Keystore Formats (PKCS12) del documento JSSE tratta nel dettaglio questo argomento. Le due system properties per specificare il tipo di store sono:
- javax.net.ssl.keyStoreType
- javax.net.ssl.trustStoreType
Nel nostro caso utilizzeremo il tipo di store di default JKS. Il documento (in realtà l'eBook) Liferay SSL/TLS Security. Come configurare il bundle Liferay per abilitare il protocollo SSL/TLS mostra un esempio di creazione ed utilizzo di keyStore in formato PKCS#12.
Una volta ottenuti keyStore e trustStore potremmo posizionarli all'interno della directory $LIFERAY_HOME/security/keystore (vedi figura 5). In alcuni casi potremmo fare anche a meno del trustStore, per esempio quando i servizi a cui vogliamo accedere presentano un certificato e una Certificate Chains trusted.
A seguire un esempio d'impostazione delle system properties per il mio ambiente Liferay 7.2 GA2. In questo caso eviteremo d'inserire la system properties che riguarda il trustStore perchè il servizio HTTPS che andremo ad utilizzare ha un certificato rilasciato da una CA (Certificate Authority) trusted (Let's Encrypt Authority X3).
-Djavax.net.ssl.keyStore=/Users/antoniomusarra/dev/liferay/liferay-ce-portal-7.2.1-ga2/security/keystore/tls-client.dontesta.it.jks -Djavax.net.ssl.keyStorePassword=secret -Djavax.net.debug=ssl
Fate attenzione alla system properties javax.net.debug=ssl, questa potrebbe essere utile in caso di troubleshooting ma in ogni caso può aiutare i neofiti a capire meglio le fasi del processo di handshake.
Quello a seguire è il contenuto integrale del file $LIFERAY_HOME/tomcat-9.0.17/bin/setenv.sh (per ambienti Unix/Linux/macOS) che imposta le system properties JSSE all'avvio del portale Liferay.
SSL_OPTS="$SSL_OPTS -Djavax.net.ssl.keyStore=/Users/antoniomusarra/dev/liferay/liferay-ce-portal-7.2.1-ga2/security/keystore/tls-client.dontesta.it.jks" SSL_OPTS="$SSL_OPTS -Djavax.net.ssl.keyStorePassword=secret" SSL_OPTS="$SSL_OPTS -Djavax.net.debug=ssl" CATALINA_OPTS="$SSL_OPTS $CATALINA_OPTS -Dfile.encoding=UTF-8 -Djava.locale.providers=JRE,COMPAT,CLDR -Djava.net.preferIPv4Stack=true -Duser.timezone=GMT -Xms2560m -Xmx2560m -XX:MaxNewSize=1536m -XX:MaxMetaspaceSize=768m -XX:MetaspaceSize=768m -XX:NewSize=1536m -XX:SurvivorRatio=7"
All'avvio del portale, grazie al fatto di aver impostato la system properties javax.net.debug=ssl, dovreste vedere in console (standard output) parecchie informazioni che riguardano il caricamento dei certificati contenuti all'interno del trustStore e keyStore. In questo caso il trustStore è quello della JVM. Per maggiori dettagli puoi consultare il file Liferay 7.2 GA2 SSL/TLS StartUp Log File.
Dopo aver configurato il portale Liferay e accertato il corretto funzionamento, il passo successivo riguarda il come implementare in modo semplice il client HTTPS con pochissime righe di codice.
5. Implementazione del Client HTTPS
Per dare subito evidenza della semplicità, potremmo realizzare un comando Gogo Shell che prenda in input l'end point del servizio e restituisca in output la risposta. Il codice a seguire mostra il comando getProtectedResource(URI). Le righe evidenziate sono le uniche fondamentali per il corretto funzionamento del client.
Il codice delle righe 41-42 inietta sul componente HTTPSProtectedResourcesCommand l'implementazione dell'interfaccia Http. Il codice della riga 17 effettua invece la chiamata verso il servizio HTTPS specificato tramite il parametro uri e accedendo in Mutua Autenticazione restituisce la risposta direttamente in console (e in formato JSON).
/** * @author Antonio Musarra */ @Component( property = { "osgi.command.function=getProtectedResource", "osgi.command.scope=security" }, service = Object.class ) public class HTTPSProtectedResourcesCommand { public void getProtectedResource(String uri) throws Exception { String response = null; try { response = _httpClient.URLtoString(uri); if (_log.isInfoEnabled()) { _log.info("Response: " + response); } System.out.println(response); } catch (IOException ioe) { if (_log.isErrorEnabled()) { _log.error(ioe.getMessage(), ioe); } throw new Exception( String.format( "They occurred in the service call. {EndPoint: %s} - {Error Message: %s}", uri, ioe.getMessage()), ioe); } } private static final Log _log = LogFactoryUtil.getLog( HTTPSProtectedResourcesCommand.class); @Reference private Http _httpClient; }
La figura 6 mostra l'output del comando g! security:getProtectedResource https://tls-auth.dontesta.it/secure/api/headers
eseguito tramite la Gogo Shell.
Sempre da figura 6, la sezione in giallo evidenzia parte del processo di handshake tra client e server. Guardando l'output completo del comando Gogo Shell security:getProtectedResource, dovreste essere in grado di identificare ogni fase del processo di handshake, così come indicato nel diagramma di figura 1.
Sempre da figura 6, la sezione in rosso evidenzia invece la risposta del servizio https://tls-auth.dontesta.it/secure/api/headers.
Sul repository GitHub amusarra/gogo-shell-tls-mutual-sample è disponibile tutto il codice sorgente del comando Gogo Shell security:getProtectedResource.
6. A quali problemi potremmo andare incontro
In questo tipo di scenario i problemi più "fastidiosi" che solitamente potremmo incontrare riguardano nella maggior parte dei casi l'errata configurazione dell'ambiente di runtime. Riporto i casi più comuni.
- Errore di accesso ai certificati (sia nel keyStore sia nel trustStore). In genere questo avviene per l'errata configurazione delle system properties; path del keyStore o trustStore errati, oppure password errata;
- Errore durante il processo di handshake. Potrebbe accadere a causa del fatto che il certificato client non venga trovato sul keyStore;
- Certificate Chains non soddisfatta.
Quelle a seguire sono le possibili eccezioni che potremmo ottenere e descritte poco prima.
java.net.SocketException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: Default, provider: SunJSSE, class: sun.security.ssl.SSLContextImpl$DefaultSSLContext) java.io.IOException: java.net.SocketException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: Default, provider: SunJSSE, class: sun.security.ssl.SSLContextImpl$DefaultSSLContext) at com.liferay.portal.util.HttpImpl.URLtoInputStream(HttpImpl.java:1985) at com.liferay.portal.util.HttpImpl.URLtoByteArray(HttpImpl.java:1669) at com.liferay.portal.util.HttpImpl.URLtoByteArray(HttpImpl.java:1190) at com.liferay.portal.util.HttpImpl.URLtoByteArray(HttpImpl.java:1203) at com.liferay.portal.util.HttpImpl.URLtoString(HttpImpl.java:1263) at it.dontesta.labs.liferay.security.tls.auth.gogoshell.HTTPSProtectedResourcesCommand.getProtectedResource(HTTPSProtectedResourcesCommand.java:54) ... Caused by: java.security.PrivilegedActionException: java.io.FileNotFoundException: /Users/antoniomusarra/dev/liferay/liferay-ce-portal-7.2.1-ga2/security/keystore/tls-client.dontesta.it (No such file or directory) at java.security.AccessController.doPrivileged(Native Method) at sun.security.ssl.SSLContextImpl$DefaultManagersHolder.getKeyManagers(SSLContextImpl.java:829) at sun.security.ssl.SSLContextImpl$DefaultManagersHolder.<clinit>(SSLContextImpl.java:765) at sun.security.ssl.SSLContextImpl$DefaultSSLContext.<init>(SSLContextImpl.java:920) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at java.security.Provider$Service.newInstance(Provider.java:1595) ... 40 more
Caused by: java.io.IOException: Keystore was tampered with, or password was incorrect at sun.security.provider.JavaKeyStore.engineLoad(JavaKeyStore.java:780) at sun.security.provider.JavaKeyStore$JKS.engineLoad(JavaKeyStore.java:56) at sun.security.provider.KeyStoreDelegator.engineLoad(KeyStoreDelegator.java:224) at sun.security.provider.JavaKeyStore$DualFormatJKS.engineLoad(JavaKeyStore.java:70) at java.security.KeyStore.load(KeyStore.java:1445) at sun.security.ssl.SSLContextImpl$DefaultManagersHolder.getKeyManagers(SSLContextImpl.java:858) at sun.security.ssl.SSLContextImpl$DefaultManagersHolder.<clinit>(SSLContextImpl.java:765) at sun.security.ssl.SSLContextImpl$DefaultSSLContext.<init>(SSLContextImpl.java:920) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at java.security.Provider$Service.newInstance(Provider.java:1595) ... 40 more Caused by: java.security.UnrecoverableKeyException: Password verification failed at sun.security.provider.JavaKeyStore.engineLoad(JavaKeyStore.java:778) ... 52 more
2020-02-21 20:54:21.991 ERROR [pipe-security:getProtectedResource https://tls-auth.dontesta.it/secure/api/headers][HTTPSProtectedResourcesCommand:64] javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure java.io.IOException: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure at com.liferay.portal.util.HttpImpl.URLtoInputStream(HttpImpl.java:1985) at com.liferay.portal.util.HttpImpl.URLtoByteArray(HttpImpl.java:1669) at com.liferay.portal.util.HttpImpl.URLtoByteArray(HttpImpl.java:1190) at com.liferay.portal.util.HttpImpl.URLtoByteArray(HttpImpl.java:1203) at com.liferay.portal.util.HttpImpl.URLtoString(HttpImpl.java:1263) at it.dontesta.labs.liferay.security.tls.auth.gogoshell.HTTPSProtectedResourcesCommand.getProtectedResource(HTTPSProtectedResourcesCommand.java:54) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.felix.gogo.runtime.Reflective.invoke(Reflective.java:139) at org.apache.felix.gogo.runtime.CommandProxy.execute(CommandProxy.java:91) at org.apache.felix.gogo.runtime.Closure.executeCmd(Closure.java:599) at org.apache.felix.gogo.runtime.Closure.executeStatement(Closure.java:526) at org.apache.felix.gogo.runtime.Closure.execute(Closure.java:415) at org.apache.felix.gogo.runtime.Pipe.doCall(Pipe.java:416) at org.apache.felix.gogo.runtime.Pipe.call(Pipe.java:229) at org.apache.felix.gogo.runtime.Pipe.call(Pipe.java:59) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.Alerts.getSSLException(Alerts.java:154) at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:2038) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1135) at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:940) at sun.security.ssl.AppInputStream.read(AppInputStream.java:105) at
... Thread-139, READ: TLSv1.2 Handshake, length = 28 *** ServerHelloDone Warning: no suitable certificate found - continuing without client authentication *** Certificate chain <Empty> Thread-139, READ: TLSv1.2 Alert, length = 26 Thread-139, RECV TLSv1.2 ALERT: fatal, handshake_failure %% Invalidated: [Session-3, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] %% Invalidated: [Session-4, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] Thread-139, called closeSocket() Thread-139, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure Thread-139, called close() Thread-139, called closeInternal(true) java.io.IOException: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure ...
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
7. Qual è stata la tua esperienza?
In questo articolo ho voluto presentare una soluzione semplice per implementare una connessione verso servizi HTTPS protetti da una meccanismo di autenticazione di tipo Two-Way SSL/TLS Mutual Authentication in un contesto Liferay.
Che tipo di esperienza hai avuto? Sei riuscito a trovare in questo articolo le informazioni necessarie per affrontare e risolvere il tuo problema?
In uno dei prossimi articoli vedremo degli scenari diversi, uno dei quali potrebbe essere la necessità di scegliere quale certificato client utilizzare per accedere ad uno specifico servizio HTTPS.
8. Risorse
Vi lascio alcune risorse che potrebbero essere utili come compendio.
- Importazione Certificati SSL sul Java Keystore (JKS)
- Liferay SSL/TLS Security. Come configurare il bundle Liferay per abilitare il protocollo SSL/TLS
- Docker Image Apache SSL/TLS Mutual Authentication on Azure Cloud
- GitHub - amusarra/docker-apache-ssl-tls-mutual-authentication
- Apache HTTP 2.4 - Docker image for SSL/TLS Mutual Authentication