Browse Source

Merge pull request #807 from csmith/certs

Initial work on certificate manager
pull/811/head
Greg Holmes 7 years ago
parent
commit
d45acfe1c8

+ 112
- 0
src/main/java/com/dmdirc/tls/CertificateExceptionManager.java View File

@@ -0,0 +1,112 @@
1
+package com.dmdirc.tls;
2
+
3
+import java.io.IOException;
4
+import java.io.InputStream;
5
+import java.io.OutputStream;
6
+import java.nio.file.Files;
7
+import java.nio.file.Path;
8
+import java.security.GeneralSecurityException;
9
+import java.security.KeyStore;
10
+import java.security.cert.PKIXParameters;
11
+import java.security.cert.TrustAnchor;
12
+import java.security.cert.X509Certificate;
13
+import java.util.Collections;
14
+import java.util.Set;
15
+import java.util.stream.Collectors;
16
+import javax.annotation.Nullable;
17
+
18
+/**
19
+ * Manages certificates that the user has explicitly trusted, excepting them from the normal PKIX checks.
20
+ */
21
+public class CertificateExceptionManager {
22
+
23
+    /** Password to use for our exception keystore. */
24
+    private static final char[] PASSWORD = "dmdirc".toCharArray();
25
+
26
+    private final Path keyStorePath;
27
+
28
+    public CertificateExceptionManager(final Path keyStorePath) {
29
+        this.keyStorePath = keyStorePath;
30
+    }
31
+
32
+    /**
33
+     * Returns the set of certificates that the user has explicitly trusted.
34
+     */
35
+    public Set<X509Certificate> getExceptedCertificates() {
36
+        try {
37
+            final KeyStore keyStore = getKeyStore();
38
+            if (keyStore == null) {
39
+                return Collections.emptySet();
40
+            } else {
41
+                final PKIXParameters params = new PKIXParameters(keyStore);
42
+                return params.getTrustAnchors().stream()
43
+                        .map(TrustAnchor::getTrustedCert)
44
+                        .collect(Collectors.toSet());
45
+            }
46
+        } catch (GeneralSecurityException | IOException e) {
47
+            return Collections.emptySet();
48
+        }
49
+    }
50
+
51
+    /**
52
+     * Adds a new certificate that will be excepted from future PKIX checks.
53
+     *
54
+     * @return True if the certificate was successfully added; false otherwise.
55
+     */
56
+    public boolean addExceptedCertificate(final X509Certificate certificate) {
57
+        try {
58
+            final KeyStore keyStore = getKeyStore();
59
+            keyStore.setCertificateEntry(
60
+                    "excepted-" + System.currentTimeMillis(),
61
+                    certificate);
62
+            try (OutputStream os = Files.newOutputStream(keyStorePath)) {
63
+                keyStore.store(os, PASSWORD);
64
+                return true;
65
+            }
66
+        } catch (GeneralSecurityException | IOException e) {
67
+            return false;
68
+        }
69
+    }
70
+
71
+    /**
72
+     * Removes a certificate that was previously added.
73
+     *
74
+     * @return True if the remove was successful; false otherwise
75
+     */
76
+    public boolean removeExceptedCertificate(final X509Certificate certificate) {
77
+        try {
78
+            final KeyStore keyStore = getKeyStore();
79
+
80
+            @Nullable final String alias = keyStore.getCertificateAlias(certificate);
81
+            if (alias == null) {
82
+                // Not found
83
+                return false;
84
+            }
85
+
86
+            keyStore.deleteEntry(alias);
87
+            try (OutputStream os = Files.newOutputStream(keyStorePath)) {
88
+                keyStore.store(os, PASSWORD);
89
+                return true;
90
+            }
91
+        } catch (GeneralSecurityException | IOException e) {
92
+            return false;
93
+        }
94
+    }
95
+
96
+    /**
97
+     * Loads the existing excepted certs KeyStore from disk, if it exists, or creates a new KeyStore otherwise.
98
+     */
99
+    private KeyStore getKeyStore() throws GeneralSecurityException, IOException {
100
+        try (InputStream is = Files.newInputStream(keyStorePath)) {
101
+            final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
102
+            keyStore.load(is, PASSWORD);
103
+            return keyStore;
104
+        } catch (GeneralSecurityException | IOException ex) {
105
+            final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
106
+            keyStore.load(null, null);
107
+            return keyStore;
108
+        }
109
+    }
110
+
111
+
112
+}

+ 12
- 27
src/main/java/com/dmdirc/tls/CertificateManager.java View File

@@ -76,12 +76,6 @@ public class CertificateManager implements X509TrustManager {
76 76
     private final AggregateConfigProvider config;
77 77
     /** The set of CAs from the global cacert file. */
78 78
     private final Set<X509Certificate> globalTrustedCAs = new HashSet<>();
79
-    /** Whether or not to the issue and expiry dates of the certificate. */
80
-    private final boolean checkDate;
81
-    /** Whether or not to the issuer of the certificate. */
82
-    private final boolean checkIssuer;
83
-    /** Whether or not to the hostname of the certificate. */
84
-    private final boolean checkHost;
85 79
     /** Used to synchronise the manager with the certificate dialog. */
86 80
     private final Semaphore actionSem = new Semaphore(0);
87 81
     /** The event bus to post errors to. */
@@ -114,9 +108,6 @@ public class CertificateManager implements X509TrustManager {
114 108
         this.connection = connection;
115 109
         this.serverName = serverName;
116 110
         this.config = config;
117
-        this.checkDate = config.getOptionBool("ssl", "checkdate");
118
-        this.checkIssuer = config.getOptionBool("ssl", "checkissuer");
119
-        this.checkHost = config.getOptionBool("ssl", "checkhost");
120 111
         this.userSettings = userSettings;
121 112
         this.eventBus = eventBus;
122 113
         this.keyStoreLocator = new KeyStoreLocator();
@@ -331,26 +322,22 @@ public class CertificateManager implements X509TrustManager {
331 322
         for (X509Certificate cert : chain) {
332 323
             final TrustResult trustResult = isTrusted(cert);
333 324
 
334
-            if (checkDate) {
335
-                // Check that the certificate is in-date
336
-                try {
337
-                    cert.checkValidity();
338
-                } catch (CertificateException ex) {
339
-                    problems.add(ex);
340
-                }
325
+            // Check that the certificate is in-date
326
+            try {
327
+                cert.checkValidity();
328
+            } catch (CertificateException ex) {
329
+                problems.add(ex);
341 330
             }
342 331
 
343
-            if (checkIssuer) {
344
-                // Check that we trust an issuer
345
-                verified |= trustResult.isTrusted();
346
-            }
332
+            // Check that we trust an issuer
333
+            verified |= trustResult.isTrusted();
347 334
 
348 335
             if (trustResult == TrustResult.TRUSTED_MANUALLY) {
349 336
                 manual = true;
350 337
             }
351 338
         }
352 339
 
353
-        if (!verified && checkIssuer) {
340
+        if (!verified) {
354 341
             problems.add(new CertificateNotTrustedException("Issuer is not trusted"));
355 342
         }
356 343
         return manual;
@@ -362,12 +349,10 @@ public class CertificateManager implements X509TrustManager {
362 349
      * @param chain The chain of certificates to check.
363 350
      */
364 351
     private void checkHost(final X509Certificate... chain) {
365
-        if (checkHost) {
366
-            // Check that the cert is issued to the correct host
367
-            if (!isValidHost(chain[0])) {
368
-                problems.add(new CertificateDoesntMatchHostException(
369
-                        "Certificate was not issued to " + serverName));
370
-            }
352
+        // Check that the cert is issued to the correct host
353
+        if (!isValidHost(chain[0])) {
354
+            problems.add(new CertificateDoesntMatchHostException(
355
+                    "Certificate was not issued to " + serverName));
371 356
         }
372 357
     }
373 358
 

+ 93
- 0
src/test/java/com/dmdirc/tls/CertificateExceptionManagerTest.java View File

@@ -0,0 +1,93 @@
1
+package com.dmdirc.tls;
2
+
3
+import java.io.IOException;
4
+import java.io.InputStream;
5
+import java.nio.file.Files;
6
+import java.nio.file.Path;
7
+import java.security.GeneralSecurityException;
8
+import java.security.KeyStore;
9
+import java.security.cert.X509Certificate;
10
+import java.util.Set;
11
+import org.junit.Before;
12
+import org.junit.Rule;
13
+import org.junit.Test;
14
+import org.junit.rules.TemporaryFolder;
15
+
16
+import static org.junit.Assert.assertEquals;
17
+import static org.junit.Assert.assertFalse;
18
+import static org.junit.Assert.assertTrue;
19
+
20
+/**
21
+ * Tests for {@link CertificateExceptionManager}.
22
+ *
23
+ * <p>These test use two certificates stored in a keystore. They were generated using:
24
+ *
25
+ * <pre>
26
+ * keytool -genkey -validity 18250 -keystore "keystore.ks" -storepass "dmdirc" -keypass "dmdirc" -alias "test1" -dname "CN=Test1, O=DMDirc, C=GB"
27
+ * keytool -genkey -validity 18250 -keystore "keystore.ks" -storepass "dmdirc" -keypass "dmdirc" -alias "test2" -dname "CN=Test2, O=DMDirc, C=GB"
28
+ * </pre>
29
+ */
30
+public class CertificateExceptionManagerTest {
31
+
32
+    @Rule
33
+    public TemporaryFolder tempFolderRule = new TemporaryFolder();
34
+
35
+    private Path keyStorePath;
36
+    private CertificateExceptionManager manager;
37
+
38
+    @Before
39
+    public void setup() throws IOException {
40
+        keyStorePath = tempFolderRule.newFile("certs.keystore").toPath();
41
+        manager = new CertificateExceptionManager(keyStorePath);
42
+    }
43
+
44
+    @Test
45
+    public void testGetCertsNoFile() {
46
+        assertTrue(manager.getExceptedCertificates().isEmpty());
47
+    }
48
+
49
+    @Test
50
+    public void testAddCert() throws GeneralSecurityException, IOException {
51
+        final X509Certificate cert = getCertificate(1);
52
+        assertTrue(manager.addExceptedCertificate(cert));
53
+        assertTrue(Files.exists(keyStorePath));
54
+        final Set<X509Certificate> certs = manager.getExceptedCertificates();
55
+        assertEquals(1, certs.size());
56
+        assertTrue(certs.contains(cert));
57
+    }
58
+
59
+    @Test
60
+    public void testRemoveUnknownCert() throws GeneralSecurityException, IOException {
61
+        final X509Certificate cert = getCertificate(1);
62
+        assertFalse(manager.removeExceptedCertificate(cert));
63
+    }
64
+
65
+    @Test
66
+    public void testRemoveCert() throws GeneralSecurityException, IOException {
67
+        final X509Certificate cert = getCertificate(1);
68
+        manager.addExceptedCertificate(cert);
69
+        assertTrue(manager.removeExceptedCertificate(cert));
70
+        assertTrue(manager.getExceptedCertificates().isEmpty());
71
+    }
72
+
73
+    @Test
74
+    public void testRemoveCertLeavesExisting() throws GeneralSecurityException, IOException {
75
+        final X509Certificate cert1 = getCertificate(1);
76
+        final X509Certificate cert2 = getCertificate(2);
77
+        manager.addExceptedCertificate(cert1);
78
+        manager.addExceptedCertificate(cert2);
79
+        assertTrue(manager.removeExceptedCertificate(cert1));
80
+        final Set<X509Certificate> certs = manager.getExceptedCertificates();
81
+        assertEquals(1, certs.size());
82
+        assertTrue(certs.contains(cert2));
83
+    }
84
+
85
+    private X509Certificate getCertificate(final int num) throws GeneralSecurityException, IOException {
86
+        try (InputStream is = getClass().getResourceAsStream("keystore.ks")) {
87
+            final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
88
+            keyStore.load(is, "dmdirc".toCharArray());
89
+            return (X509Certificate) keyStore.getCertificate("test" + num);
90
+        }
91
+    }
92
+
93
+}

BIN
src/test/resources/com/dmdirc/tls/keystore.ks View File


Loading…
Cancel
Save