/*
*
* Copyright 2013 Entando S.r.l. (http://www.entando.com) All rights reserved.
*
* This file is part of Entando Enterprise Edition software.
* You can redistribute it and/or modify it
* under the terms of the Entando's EULA
*
* See the file License for the specific language governing permissions
* and limitations under the License
*
*
*
* Copyright 2013 Entando S.r.l. (http://www.entando.com) All rights reserved.
*
*/
package com.agiletec.plugins.jpwebmail.aps.system.services.webmail.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import com.agiletec.aps.system.ApsSystemUtils;
import com.agiletec.aps.system.exception.ApsSystemException;
import com.agiletec.plugins.jpwebmail.aps.system.JpwebmailSystemConstants;
import com.agiletec.plugins.jpwebmail.aps.system.services.webmail.WebMailConfig;
public class CertificateHandler {
/**
* Attiva il meccanismo che porta alla negoziazione dei certificati di sicurezza; SE l'handshake è stato effettuato allora ritorna
* immediatamente senza modificare alcun parametro.
*
* @param host
* @param port
* @param protocol
* @throws ApsSystemException in caso di errore nell'elaborazione dei certificati
*/
public void aquireCertificate(String host, Integer port, String protocol, WebMailConfig config) throws ApsSystemException {
boolean proceed;
this.setConfig(config);
if (!this._checkPerformed) {
proceed = config.isCertificateEnable();
if (null == port || null == host || null == protocol) {
_mustCheckCertificates = false;
throw new ApsSystemException(showConsoleInfo("Parametri per l'handshake SSL non validi, negoziazione certificati SSL cancellata"));
} else {
_mustCheckCertificates = true;
}
if ((!protocol.equalsIgnoreCase("imaps") && !protocol.equalsIgnoreCase("imaps")) || !proceed) {
_mustCheckCertificates = false;
}
// infine...
if (_mustCheckCertificates) {
try {
// esegui l'handshake con l'host
doSSLHandshake(host,port,protocol);
// modifica le proprietà del sistema
System.setProperty("javax.net.ssl.trustStore", _certificateInUse);
System.setProperty("javax.net.ssl.trustStorePassword", this.getPasswordInUse());
System.setProperty("javax.net.ssl.trustStoreType","JKS");
ApsSystemUtils.getLogger().info("'"+_certificateInUse+"' e password inseriti nelle proprietà di sistema");
} catch (Throwable t) {
ApsSystemUtils.getLogger().throwing(this.getClass().getName(), "Certificates ["+t.getLocalizedMessage()+"]",t);
}
} else {
this._checkPerformed = true;
ApsSystemUtils.getLogger().info(showConsoleInfo("Il controllo del certificato SSL è stato disabilitato da configurazione oppure il protocollo '"+protocol+"' non lo prevede"));
}
}
}
private String showConsoleInfo(String msg) {
Boolean debugOnConsole = this.getConfig().isCertificateDebugOnConsole();
if (debugOnConsole)
System.out.println(msg);
return msg;
}
private KeyStore doKeyStore(File file, char[] passphrase) throws Throwable {
KeyStore ks;
InputStream in = null;
if (null == file) {
throw new ApsSystemException("Il file fornito per il keyStore non è valido");
}
in = new FileInputStream(file);
ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(in, passphrase);
in.close();
return ks;
}
private File getCertificatesFile(String name) throws Throwable {
String certificatePath = this.getConfig().getCertificatePath();
String java_home = System.getProperty("java.home");
File certificateDirectoryPath = new File(certificatePath);
boolean pathDirsCreated;
char SEP = File.separatorChar;
File file = null;
// controllo per l'esistenza dei parametri richiesti
if (null == name) {
ApsSystemUtils.getLogger().info(showConsoleInfo("il nome del certificato non è valido, uso il default 'jssecacerts'"));
name = "jssecacerts";
}
if (null != java_home && java_home.length()==0 && this._mustCheckCertificates) {
throw new ApsSystemException(showConsoleInfo("Variabile 'JAVA_HOME' non definita, richiesta se 'Enable' in 'certificate' è vera"));
}
if (null==certificatePath) {
throw new ApsSystemException(showConsoleInfo("Variabile 'certificatePath' non definita, richiesta se 'Enable' in 'certificate' è vera"));
}
// ottiene e crea un nuovo certificato locale
file = new File(certificatePath+SEP+name);
// crea le directory intermedie
pathDirsCreated = certificateDirectoryPath.mkdirs();
if (!pathDirsCreated && !certificateDirectoryPath.exists()) {
ApsSystemUtils.getLogger().info(showConsoleInfo("ATTENZIONE: non è stato possibile creare tutte le directory intermedie del percorso '"+file+"'"));
}
if (file.isFile() == false) {
String workingDirectory = java_home + SEP + "lib" + SEP + "security";
File dir = new File(workingDirectory);
ApsSystemUtils.getLogger().info(showConsoleInfo("directory di lavoro '"+workingDirectory)+"'");
file = new File(dir, name);
if (file.isFile() == false) {
file = new File(dir, "cacerts");
ApsSystemUtils.getLogger().info(showConsoleInfo("creazione di un nuovo certificato '"+file+"'"));
}
} else {
ApsSystemUtils.getLogger().info(showConsoleInfo("certificato '"+name+"' acquisito in '"+certificatePath+"'"));
}
this._certificateInUse = file.toString();
return file;
}
private void doSSLHandshake(String host, Integer port, String protocol) throws Throwable {
String certificatePath = this.getConfig().getCertificatePath();
char SEP = File.separatorChar;
SSLContext context = null;
TrustManagerFactory trustManagerFactory = null;
X509TrustManager defaultTrustManager = null;
KeyStore keyStore = null;
SimpleTrustManager trustManager = null;
SSLSocket socket = null;
SSLSocketFactory factory = null;
X509Certificate[] chain;
MessageDigest sha1 = null;
MessageDigest md5 = null;
File file = null;
String alias;
final String certificateFile = "jssecacerts";
file = getCertificatesFile(certificateFile);
context = SSLContext.getInstance("TLS");
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
ApsSystemUtils.getLogger().info(showConsoleInfo("Caricamento file KeyStore '" + file + "'..."));
keyStore=doKeyStore(file, this.getPasswordInUse().toCharArray());
trustManagerFactory.init(keyStore);
defaultTrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
trustManager = new SimpleTrustManager(defaultTrustManager);
context.init(null, new TrustManager[] {trustManager}, null);
factory = context.getSocketFactory();
ApsSystemUtils.getLogger().info(showConsoleInfo("SSL handshake con l'host '" + host + ":" + port + "'..."));
socket = (SSLSocket)factory.createSocket(host, port);
socket.setSoTimeout(10000);
try {
this._checkPerformed = true;
socket.startHandshake();
socket.close();
ApsSystemUtils.getLogger().info(showConsoleInfo("\nCertificato dell'host '"+host+"' fidato"));
this._hostTrusted = true;
this._certificateInUse = file.toString();
return;
} catch (SSLException e) {
ApsSystemUtils.getLogger().info(showConsoleInfo("\nIl certificato dell'host '"+host+"' non è fidato"));
// a questo punto l'host non è nella lista dei server fidati quindi se 'lazyCheck' è posto a false allora segnaliamo l'errore
if (!this.getConfig().isCertificateLazyCheck()) {
this._hostTrusted=false;
ApsSystemUtils.getLogger().info(showConsoleInfo("Il certificato per l'host '"+host+":"+port+"' non è stato accettato"));
}
chain = trustManager.getChain();
if (chain == null) {
showConsoleInfo("Errore nell'ottenere la catena dei server certificati");
ApsSystemUtils.getLogger().info("Errore nell'ottenere la catena dei server certificati");
} else {
sha1 = MessageDigest.getInstance("SHA1");
md5 = MessageDigest.getInstance("MD5");
ApsSystemUtils.getLogger().info(showConsoleInfo("L'host '" + host + "' ha spedito " + chain.length + " certificato/i"));
for (int i = 0; i < chain.length; i++) {
X509Certificate cert = chain[i];
sha1.update(cert.getEncoded());
md5.update(cert.getEncoded());
}
OutputStream out = new FileOutputStream(certificatePath+SEP+certificateFile);
for (int s=0;s < chain.length; s++) {
X509Certificate cert = chain[s];
alias = host + "-" + (s + 1);
keyStore.setCertificateEntry(alias, cert);
ApsSystemUtils.getLogger().info(showConsoleInfo("Aggiunta del certificato in '"+certificatePath+SEP+certificateFile+"'..."));
keyStore.store(out, this.getPasswordInUse().toCharArray());
ApsSystemUtils.getLogger().info(showConsoleInfo("Certificato aggiunto con l'alias '"+alias+"'"));
}
out.close();
this._hostTrusted = true;
this._certificateInUse = certificatePath+SEP+certificateFile;
}
}
}
/**
* Restituisce 'true' se e solo se l'host è ritenuto affidabile. Scatena un'eccezione se viene invocato prima che l'handshake
* venga effettuato, qualora 'enabled' sia posto a 'true'
*/
public boolean isHostTrusted() throws ApsSystemException {
if (this._checkPerformed) {
return this._hostTrusted && this._mustCheckCertificates;
} else {
if (this.getConfig().isCertificateEnable()) {
throw new ApsSystemException("'isCertificateAquired' invocato prima che l'handshake fosse effettuato");
}
}
return false;
}
/**
* Questo metodo indica se sia possibile procedere con la connessione in maniera 'affidabile'.
* NOTA BENE: SE l'handshaking è stato disabilitato allora restituisce 'true' (contestualmente 'isHostTrusted' risulta 'false')
*
* @return 'true' se e solo se l'host è ritenuto affidabile oppure se il meccanismo di handshking risulta disabilitato.
*/
public boolean proceedWithConnection() throws ApsSystemException {
boolean isEnabled = this.getConfig().isCertificateEnable();
return (isHostTrusted() && isEnabled) || !isEnabled;
}
public WebMailConfig getConfig() {
return _config;
}
protected void setConfig(WebMailConfig config) {
this._config = config;
}
/**
* Esporta il file usato per la registrazione dei certificati.
*
* @return il file contenente il certificato in uso.
*/
public String getCertificateInUse() {
return _certificateInUse;
}
/**
* Restituisce la password usata per registrare in locale i certificati autogenerati
*
* @ return la password usata per registrare i certificati autogenerati
*/
public String getPasswordInUse() {
return JpwebmailSystemConstants.WEBMAIL_SSL_CERTIFICATE_STORE_PWD;
}
/**
* Se il protocollo è diverso da 'imaps' o 'pop3s' oppure se l'acquisizione dei certificati è disabilitata
* allora '_mustCheckCertificates' viene posto a 'false'. Di conseguenza, l'handshake non sarà mai effettuato E
* '_hostTrusted' sarà sempre 'false'.
*/
private boolean _mustCheckCertificates;
private boolean _hostTrusted;
private boolean _checkPerformed = false;
private String _certificateInUse;
private WebMailConfig _config;
}