summaryrefslogtreecommitdiff
path: root/platform/platform-api/src/com/intellij/util/net/ssl/ConfirmingTrustManager.java
blob: 82dd36e7f41e894a305bfee9d22015da7e2db7d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
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.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];
    if (app.isUnitTestMode() || app.isHeadlessEnvironment()) {
      myCustomManager.addCertificate(endPoint);
      return true;
    }
    boolean accepted = CertificatesManager.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");
      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();
      }
    }

    @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);
      }
    }
  }
}