Browse Source

Add manager for user-trusted certificates.

When the user manually trusts a certificate, we should be storing
the whole cert instead of just an encoding of its fingerprint.

This allows us to display it properly, chain other certs trusted
by it, and generally do everything more sanely.

Baby step for issue #806
pull/807/head
Chris Smith 7 years ago
parent
commit
40e617a9c2

+ 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
+}

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

@@ -0,0 +1,84 @@
1
+package com.dmdirc.tls;
2
+
3
+import java.io.IOException;
4
+import java.nio.file.Files;
5
+import java.nio.file.Path;
6
+import java.security.GeneralSecurityException;
7
+import java.security.cert.X509Certificate;
8
+import java.util.Set;
9
+import org.junit.Before;
10
+import org.junit.Rule;
11
+import org.junit.Test;
12
+import org.junit.rules.TemporaryFolder;
13
+import sun.security.tools.keytool.CertAndKeyGen;
14
+import sun.security.x509.X500Name;
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
+public class CertificateExceptionManagerTest {
24
+
25
+    @Rule
26
+    public TemporaryFolder tempFolderRule = new TemporaryFolder();
27
+
28
+    private Path keyStorePath;
29
+    private CertificateExceptionManager manager;
30
+
31
+    @Before
32
+    public void setup() throws IOException {
33
+        keyStorePath = tempFolderRule.newFile("certs.keystore").toPath();
34
+        manager = new CertificateExceptionManager(keyStorePath);
35
+    }
36
+
37
+    @Test
38
+    public void testGetCertsNoFile() {
39
+        assertTrue(manager.getExceptedCertificates().isEmpty());
40
+    }
41
+
42
+    @Test
43
+    public void testAddCert() throws GeneralSecurityException, IOException {
44
+        X509Certificate cert = generateCertificate();
45
+        assertTrue(manager.addExceptedCertificate(cert));
46
+        assertTrue(Files.exists(keyStorePath));
47
+        Set<X509Certificate> certs = manager.getExceptedCertificates();
48
+        assertEquals(1, certs.size());
49
+        assertTrue(certs.contains(cert));
50
+    }
51
+
52
+    @Test
53
+    public void testRemoveUnknownCert() throws GeneralSecurityException, IOException {
54
+        X509Certificate cert = generateCertificate();
55
+        assertFalse(manager.removeExceptedCertificate(cert));
56
+    }
57
+
58
+    @Test
59
+    public void testRemoveCert() throws GeneralSecurityException, IOException {
60
+        X509Certificate cert = generateCertificate();
61
+        manager.addExceptedCertificate(cert);
62
+        assertTrue(manager.removeExceptedCertificate(cert));
63
+        assertTrue(manager.getExceptedCertificates().isEmpty());
64
+    }
65
+
66
+    @Test
67
+    public void testRemoveCertLeavesExisting() throws GeneralSecurityException, IOException {
68
+        X509Certificate cert1 = generateCertificate();
69
+        X509Certificate cert2 = generateCertificate();
70
+        manager.addExceptedCertificate(cert1);
71
+        manager.addExceptedCertificate(cert2);
72
+        assertTrue(manager.removeExceptedCertificate(cert1));
73
+        Set<X509Certificate> certs = manager.getExceptedCertificates();
74
+        assertEquals(1, certs.size());
75
+        assertTrue(certs.contains(cert2));
76
+    }
77
+
78
+    private X509Certificate generateCertificate() throws GeneralSecurityException, IOException {
79
+        CertAndKeyGen certGen = new CertAndKeyGen("RSA", "SHA256WithRSA", null);
80
+        certGen.generate(2048);
81
+        return certGen.getSelfCertificate(new X500Name("CN=Test,O=DMDirc,C=GB"), 120);
82
+    }
83
+
84
+}

Loading…
Cancel
Save