blob: a955216cef90b1c56c2511ea4bcaec4f96ca5989 [file] [log] [blame]
package com.intellij.util.net.ssl;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.StreamUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.EventDispatcher;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* The central piece of our SSL support - special kind of trust manager, that asks user to confirm
* untrusted certificate, e.g. if it wasn't found in system-wide storage.
*
* @author Mikhail Golubev
*/
public class ConfirmingTrustManager extends ClientOnlyTrustManager {
private static final Logger LOG = Logger.getInstance(ConfirmingTrustManager.class);
private static final X509Certificate[] NO_CERTIFICATES = new X509Certificate[0];
private static final X509TrustManager MISSING_TRUST_MANAGER = new ClientOnlyTrustManager() {
@Override
public void checkServerTrusted(X509Certificate[] certificates, String s) throws CertificateException {
LOG.debug("Trust manager is missing. Retreating.");
throw new CertificateException("Missing trust manager");
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return NO_CERTIFICATES;
}
};
public static ConfirmingTrustManager createForStorage(@NotNull String path, @NotNull String password) {
return new ConfirmingTrustManager(getSystemDefault(), new MutableTrustManager(path, password));
}
private static X509TrustManager getSystemDefault() {
try {
TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// hacky way to get default trust store
factory.init((KeyStore)null);
// assume that only X509 TrustManagers exist
X509TrustManager systemManager = findX509TrustManager(factory.getTrustManagers());
if (systemManager != null && systemManager.getAcceptedIssuers().length != 0) {
return systemManager;
}
}
catch (Exception e) {
LOG.error("Cannot get system trust store", e);
}
return MISSING_TRUST_MANAGER;
}
private final X509TrustManager mySystemManager;
private final MutableTrustManager myCustomManager;
private ConfirmingTrustManager(X509TrustManager system, MutableTrustManager custom) {
mySystemManager = system;
myCustomManager = custom;
}
private static X509TrustManager findX509TrustManager(TrustManager[] managers) {
for (TrustManager manager : managers) {
if (manager instanceof X509TrustManager) {
return (X509TrustManager)manager;
}
}
return null;
}
@Override
public void checkServerTrusted(final X509Certificate[] certificates, String s) throws CertificateException {
try {
mySystemManager.checkServerTrusted(certificates, s);
}
catch (CertificateException e) {
// check-then-act sequence
synchronized (myCustomManager) {
try {
myCustomManager.checkServerTrusted(certificates, s);
}
catch (CertificateException e2) {
if (myCustomManager.isBroken() || !confirmAndUpdate(certificates)) {
throw e;
}
}
}
}
}
private boolean confirmAndUpdate(final X509Certificate[] chain) {
Application app = ApplicationManager.getApplication();
final X509Certificate endPoint = chain[0];
// IDEA-123467 and IDEA-123335 workaround
String threadClassName = StringUtil.notNullize(Thread.currentThread().getClass().getCanonicalName());
if (threadClassName.equals("sun.awt.image.ImageFetcher")) {
LOG.debug("Image Fetcher thread is detected. Certificate check will be skipped.");
return true;
}
CertificateManager.Config config = CertificateManager.getInstance().getState();
if (app.isUnitTestMode() || app.isHeadlessEnvironment() || config.ACCEPT_AUTOMATICALLY) {
LOG.debug("Certificate will be accepted automatically");
myCustomManager.addCertificate(endPoint);
return true;
}
boolean accepted = CertificateManager.showAcceptDialog(new Callable<DialogWrapper>() {
@Override
public DialogWrapper call() throws Exception {
// TODO may be another kind of warning, if default trust store is missing
return CertificateWarningDialog.createUntrustedCertificateWarning(endPoint);
}
});
if (accepted) {
LOG.info("Certificate was accepted by user");
myCustomManager.addCertificate(endPoint);
}
return accepted;
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return ArrayUtil.mergeArrays(mySystemManager.getAcceptedIssuers(), myCustomManager.getAcceptedIssuers());
}
public X509TrustManager getSystemManager() {
return mySystemManager;
}
public MutableTrustManager getCustomManager() {
return myCustomManager;
}
/**
* Trust manager that supports modifications of underlying physical key store.
* It can also notify clients about such modifications, see {@link #addListener(CertificateListener)}.
*
* @see com.intellij.util.net.ssl.CertificateListener
*/
public static class MutableTrustManager extends ClientOnlyTrustManager {
private final String myPath;
private final String myPassword;
private final TrustManagerFactory myFactory;
private final KeyStore myKeyStore;
private final ReadWriteLock myLock = new ReentrantReadWriteLock();
private final Lock myReadLock = myLock.readLock();
private final Lock myWriteLock = myLock.writeLock();
// reloaded after each modification
private X509TrustManager myTrustManager;
private final EventDispatcher<CertificateListener> myDispatcher = EventDispatcher.create(CertificateListener.class);
private MutableTrustManager(@NotNull String path, @NotNull String password) {
myPath = path;
myPassword = password;
// initialization step
myWriteLock.lock();
try {
myFactory = createFactory();
myKeyStore = createKeyStore(path, password);
myTrustManager = initFactoryAndGetManager();
}
finally {
myWriteLock.unlock();
}
}
private static TrustManagerFactory createFactory() {
try {
return TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
}
catch (NoSuchAlgorithmException e) {
return null;
}
}
private static KeyStore createKeyStore(@NotNull String path, @NotNull String password) {
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
File cacertsFile = new File(path);
if (cacertsFile.exists()) {
FileInputStream stream = null;
try {
stream = new FileInputStream(path);
keyStore.load(stream, password.toCharArray());
}
finally {
StreamUtil.closeStream(stream);
}
}
else {
if (!FileUtil.createParentDirs(cacertsFile)) {
LOG.error("Cannot create directories: " + cacertsFile.getParent());
return null;
}
keyStore.load(null, password.toCharArray());
}
}
catch (Exception e) {
LOG.error(e);
return null;
}
return keyStore;
}
/**
* Add certificate to underlying trust store.
*
* @param certificate server's certificate
* @return whether the operation was successful
*/
public boolean addCertificate(@NotNull X509Certificate certificate) {
myWriteLock.lock();
try {
if (isBroken()) {
return false;
}
myKeyStore.setCertificateEntry(createAlias(certificate), certificate);
flushKeyStore();
// trust manager should be updated each time its key store was modified
myTrustManager = initFactoryAndGetManager();
myDispatcher.getMulticaster().certificateAdded(certificate);
return true;
}
catch (Exception e) {
LOG.error("Can't add certificate", e);
return false;
}
finally {
myWriteLock.unlock();
}
}
/**
* Add certificate, loaded from file at {@code path}, to underlying trust store.
*
* @param path path to file containing certificate
* @return whether the operation was successful
*/
public boolean addCertificate(@NotNull String path) {
X509Certificate certificate = CertificateUtil.loadX509Certificate(path);
return certificate != null && addCertificate(certificate);
}
private static String createAlias(@NotNull X509Certificate certificate) {
return CertificateUtil.getCommonName(certificate);
}
/**
* Remove certificate from underlying trust store.
*
* @param certificate certificate alias
* @return whether the operation was successful
*/
public boolean removeCertificate(@NotNull X509Certificate certificate) {
return removeCertificate(createAlias(certificate));
}
/**
* Remove certificate, specified by its alias, from underlying trust store.
*
* @param alias certificate's alias
* @return true if removal operation was successful and false otherwise
*/
public boolean removeCertificate(@NotNull String alias) {
myWriteLock.lock();
try {
if (isBroken()) {
return false;
}
// for listeners
X509Certificate certificate = getCertificate(alias);
if (certificate == null) {
LOG.error("No certificate found for alias: " + alias);
return false;
}
myKeyStore.deleteEntry(alias);
flushKeyStore();
// trust manager should be updated each time its key store was modified
myTrustManager = initFactoryAndGetManager();
myDispatcher.getMulticaster().certificateRemoved(certificate);
return true;
}
catch (Exception e) {
LOG.error("Can't remove certificate for alias: " + alias, e);
return false;
}
finally {
myWriteLock.unlock();
}
}
/**
* Get certificate, specified by its alias, from underlying trust store.
*
* @param alias certificate's alias
* @return certificate or null if it's not present
*/
@Nullable
public X509Certificate getCertificate(@NotNull String alias) {
myReadLock.lock();
try {
return (X509Certificate)myKeyStore.getCertificate(alias);
}
catch (KeyStoreException e) {
return null;
}
finally {
myReadLock.unlock();
}
}
/**
* Select all available certificates from underlying trust store. Returned list is not supposed to be modified.
*
* @return certificates
*/
public List<X509Certificate> getCertificates() {
myReadLock.lock();
try {
List<X509Certificate> certificates = new ArrayList<X509Certificate>();
for (String alias : Collections.list(myKeyStore.aliases())) {
certificates.add(getCertificate(alias));
}
return ContainerUtil.immutableList(certificates);
}
catch (Exception e) {
LOG.error(e);
return ContainerUtil.emptyList();
}
finally {
myReadLock.unlock();
}
}
/**
* Check that underlying trust store contains certificate with specified alias.
*
* @param alias - certificate's alias to be checked
* @return - whether certificate is in storage
*/
public boolean containsCertificate(@NotNull String alias) {
myReadLock.lock();
try {
return myKeyStore.containsAlias(alias);
}
catch (KeyStoreException e) {
LOG.error(e);
return false;
} finally {
myReadLock.unlock();
}
}
boolean removeAllCertificates() {
for (X509Certificate certificate : getCertificates()) {
if (!removeCertificate(certificate)) {
return false;
}
}
return true;
}
@Override
public void checkServerTrusted(X509Certificate[] certificates, String s) throws CertificateException {
myReadLock.lock();
try {
if (keyStoreIsEmpty() || isBroken()) {
throw new CertificateException();
}
myTrustManager.checkServerTrusted(certificates, s);
}
finally {
myReadLock.unlock();
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
myReadLock.lock();
try {
// trust no one if broken
if (keyStoreIsEmpty() || isBroken()) {
return NO_CERTIFICATES;
}
return myTrustManager.getAcceptedIssuers();
}
finally {
myReadLock.unlock();
}
}
public void addListener(@NotNull CertificateListener listener) {
myDispatcher.addListener(listener);
}
public void removeListener(@NotNull CertificateListener listener) {
myDispatcher.removeListener(listener);
}
// Guarded by caller's lock
private boolean keyStoreIsEmpty() {
try {
return myKeyStore.size() == 0;
}
catch (KeyStoreException e) {
LOG.error(e);
return true;
}
}
// Guarded by caller's lock
private X509TrustManager initFactoryAndGetManager() {
try {
if (myFactory != null && myKeyStore != null) {
myFactory.init(myKeyStore);
return findX509TrustManager(myFactory.getTrustManagers());
}
}
catch (KeyStoreException e) {
LOG.error(e);
}
return null;
}
// Guarded by caller's lock
private boolean isBroken() {
return myKeyStore == null || myFactory == null || myTrustManager == null;
}
private void flushKeyStore() throws Exception {
FileOutputStream stream = new FileOutputStream(myPath);
try {
myKeyStore.store(stream, myPassword.toCharArray());
}
finally {
StreamUtil.closeStream(stream);
}
}
}
}