View Javadoc
1   /*
2    * Copyright 2019-2021 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package nl.altindag.ssl.util;
18  
19  import nl.altindag.log.LogCaptor;
20  import nl.altindag.ssl.SSLFactory;
21  import nl.altindag.ssl.exception.GenericKeyStoreException;
22  import org.junit.jupiter.api.Test;
23  import org.junit.jupiter.api.extension.ExtendWith;
24  import org.mockito.MockedStatic;
25  import org.mockito.junit.jupiter.MockitoExtension;
26  
27  import javax.net.ssl.X509ExtendedTrustManager;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.lang.reflect.Method;
31  import java.nio.file.Files;
32  import java.nio.file.OpenOption;
33  import java.nio.file.Path;
34  import java.nio.file.Paths;
35  import java.nio.file.StandardOpenOption;
36  import java.security.KeyStore;
37  import java.security.KeyStoreException;
38  import java.security.cert.Certificate;
39  import java.security.cert.X509Certificate;
40  import java.util.List;
41  
42  import static nl.altindag.ssl.TestConstants.IDENTITY_FILE_NAME;
43  import static nl.altindag.ssl.TestConstants.KEYSTORE_LOCATION;
44  import static nl.altindag.ssl.TestConstants.TRUSTSTORE_FILE_NAME;
45  import static nl.altindag.ssl.TestConstants.TRUSTSTORE_PASSWORD;
46  import static org.assertj.core.api.Assertions.assertThat;
47  import static org.assertj.core.api.Assertions.assertThatThrownBy;
48  import static org.mockito.ArgumentMatchers.any;
49  import static org.mockito.ArgumentMatchers.anyString;
50  import static org.mockito.Mockito.doThrow;
51  import static org.mockito.Mockito.mock;
52  import static org.mockito.Mockito.mockStatic;
53  import static org.mockito.Mockito.spy;
54  
55  /**
56   * @author Hakan Altindag
57   */
58  @ExtendWith(MockitoExtension.class)
59  class KeyStoreUtilsShould {
60  
61      private static final String JCEKS_KEYSTORE_FILE_NAME = "identity.jceks";
62      private static final String PKCS12_KEYSTORE_FILE_NAME = "identity.p12";
63      private static final String NON_EXISTING_KEYSTORE_FILE_NAME = "black-hole.jks";
64  
65      private static final char[] KEYSTORE_PASSWORD = new char[] {'s', 'e', 'c', 'r', 'e', 't'};
66      private static final String ORIGINAL_OS_NAME = System.getProperty("os.name");
67      private static final String TEST_RESOURCES_LOCATION = "src/test/resources/";
68  
69      @Test
70      void loadKeyStoreFromClasspath() {
71          KeyStore keyStore = KeyStoreUtils.loadKeyStore(KEYSTORE_LOCATION + IDENTITY_FILE_NAME, KEYSTORE_PASSWORD);
72          assertThat(keyStore).isNotNull();
73      }
74  
75      @Test
76      void loadJCEKSKeyStoreFromClasspath() {
77          KeyStore keyStore = KeyStoreUtils.loadKeyStore(KEYSTORE_LOCATION + JCEKS_KEYSTORE_FILE_NAME, KEYSTORE_PASSWORD, "JCEKS");
78          assertThat(keyStore).isNotNull();
79      }
80  
81      @Test
82      void loadPKCS12KeyStoreFromClasspath() {
83          KeyStore keyStore = KeyStoreUtils.loadKeyStore(KEYSTORE_LOCATION + PKCS12_KEYSTORE_FILE_NAME, KEYSTORE_PASSWORD, "PKCS12");
84          assertThat(keyStore).isNotNull();
85      }
86  
87      @Test
88      void loadKeyStoreWithPathFromDirectory() {
89          KeyStore keyStore = KeyStoreUtils.loadKeyStore(Paths.get(TEST_RESOURCES_LOCATION + KEYSTORE_LOCATION + IDENTITY_FILE_NAME).toAbsolutePath(), KEYSTORE_PASSWORD);
90          assertThat(keyStore).isNotNull();
91      }
92  
93      @Test
94      void loadKeyStoreAsInputStream() throws IOException {
95          KeyStore keyStore;
96          try(InputStream inputStream = getResource(KEYSTORE_LOCATION + IDENTITY_FILE_NAME)) {
97              keyStore = KeyStoreUtils.loadKeyStore(inputStream, "secret".toCharArray());
98          }
99  
100         assertThat(keyStore).isNotNull();
101     }
102 
103     @Test
104     void loadSystemKeyStore() {
105         List<KeyStore> keyStores = KeyStoreUtils.loadSystemKeyStores();
106 
107         String operatingSystem = System.getProperty("os.name").toLowerCase();
108         if (operatingSystem.contains("mac") || operatingSystem.contains("windows")) {
109             assertThat(keyStores).isNotEmpty();
110         }
111     }
112 
113     @Test
114     void loadWindowsSystemKeyStore() {
115         System.setProperty("os.name", "windows");
116         KeyStore windowsRootKeyStore = mock(KeyStore.class);
117         KeyStore windowsMyKeyStore = mock(KeyStore.class);
118 
119         try (MockedStatic<KeyStoreUtils> keyStoreUtilsMock = mockStatic(KeyStoreUtils.class, invocation -> {
120             Method method = invocation.getMethod();
121             if ("loadSystemKeyStores".equals(method.getName()) && method.getParameterCount() == 0) {
122                 return invocation.callRealMethod();
123             } else {
124                 return invocation.getMock();
125             }
126         })) {
127             keyStoreUtilsMock.when(() -> KeyStoreUtils.createKeyStore("Windows-ROOT", null)).thenReturn(windowsRootKeyStore);
128             keyStoreUtilsMock.when(() -> KeyStoreUtils.createKeyStore("Windows-MY", null)).thenReturn(windowsMyKeyStore);
129 
130             List<KeyStore> keyStores = KeyStoreUtils.loadSystemKeyStores();
131             assertThat(keyStores).containsExactlyInAnyOrder(windowsRootKeyStore, windowsMyKeyStore);
132         }
133 
134         resetOsName();
135     }
136 
137     @Test
138     void loadMacSystemKeyStore() {
139         System.setProperty("os.name", "mac");
140         KeyStore macKeyStore = mock(KeyStore.class);
141 
142         try (MockedStatic<KeyStoreUtils> keyStoreUtilsMock = mockStatic(KeyStoreUtils.class, invocation -> {
143             Method method = invocation.getMethod();
144             if ("loadSystemKeyStores".equals(method.getName()) && method.getParameterCount() == 0) {
145                 return invocation.callRealMethod();
146             } else {
147                 return invocation.getMock();
148             }
149         })) {
150             keyStoreUtilsMock.when(() -> KeyStoreUtils.createKeyStore("KeychainStore", null)).thenReturn(macKeyStore);
151 
152             List<KeyStore> keyStores = KeyStoreUtils.loadSystemKeyStores();
153             assertThat(keyStores).containsExactly(macKeyStore);
154         }
155 
156         resetOsName();
157     }
158 
159     @Test
160     void loadLinuxSystemKeyStoreReturnsEmptyList() {
161         System.setProperty("os.name", "linux");
162 
163         LogCaptor logCaptor = LogCaptor.forClass(KeyStoreUtils.class);
164 
165         List<KeyStore> trustStores = KeyStoreUtils.loadSystemKeyStores();
166 
167         assertThat(trustStores).isEmpty();
168         assertThat(logCaptor.getWarnLogs())
169                 .hasSize(1)
170                 .contains("No system KeyStores available for [linux]");
171 
172         resetOsName();
173     }
174 
175     @Test
176     void createTrustStoreFromX509CertificatesAsList() throws KeyStoreException {
177         SSLFactory sslFactory = SSLFactory.builder()
178                 .withTrustMaterial(KEYSTORE_LOCATION + TRUSTSTORE_FILE_NAME, TRUSTSTORE_PASSWORD)
179                 .withDefaultTrustMaterial()
180                 .build();
181 
182         List<X509Certificate> trustedCertificates = sslFactory.getTrustedCertificates();
183         KeyStore trustStore = KeyStoreUtils.createTrustStore(trustedCertificates);
184 
185         assertThat(trustedCertificates.size()).isNotZero();
186         assertThat(trustStore.size()).isNotZero();
187         assertThat(trustStore.size()).isEqualTo(trustedCertificates.size());
188     }
189 
190     @Test
191     void createTrustStoreFromX509CertificatesAsArray() throws KeyStoreException {
192         SSLFactory sslFactory = SSLFactory.builder()
193                 .withTrustMaterial(KEYSTORE_LOCATION + TRUSTSTORE_FILE_NAME, TRUSTSTORE_PASSWORD)
194                 .withDefaultTrustMaterial()
195                 .build();
196 
197         X509Certificate[] trustedCertificates = sslFactory.getTrustedCertificates().toArray(new X509Certificate[0]);
198         KeyStore trustStore = KeyStoreUtils.createTrustStore(trustedCertificates);
199 
200         assertThat(trustedCertificates.length).isNotZero();
201         assertThat(trustStore.size()).isNotZero();
202         assertThat(trustStore.size()).isEqualTo(trustedCertificates.length);
203     }
204 
205     @Test
206     void createTrustStoreFromMultipleTrustManagers() throws KeyStoreException {
207         X509ExtendedTrustManager jdkTrustManager = TrustManagerUtils.createTrustManagerWithJdkTrustedCertificates();
208         X509ExtendedTrustManager customTrustManager = TrustManagerUtils.createTrustManager(KeyStoreUtils.loadKeyStore(KEYSTORE_LOCATION + TRUSTSTORE_FILE_NAME, TRUSTSTORE_PASSWORD));
209 
210         KeyStore trustStore = KeyStoreUtils.createTrustStore(jdkTrustManager, customTrustManager);
211 
212         assertThat(jdkTrustManager.getAcceptedIssuers().length).isNotZero();
213         assertThat(customTrustManager.getAcceptedIssuers().length).isNotZero();
214         assertThat(trustStore.size()).isNotZero();
215         assertThat(trustStore.size()).isEqualTo(jdkTrustManager.getAcceptedIssuers().length + customTrustManager.getAcceptedIssuers().length);
216     }
217 
218     @Test
219     void throwExceptionWhenLoadingNonExistingKeystoreType() {
220         assertThatThrownBy(() -> KeyStoreUtils.loadKeyStore(KEYSTORE_LOCATION + IDENTITY_FILE_NAME, KEYSTORE_PASSWORD, "unknown"))
221                 .isInstanceOf(GenericKeyStoreException.class)
222                 .hasMessageContaining("unknown not found");
223     }
224 
225     @Test
226     void throwExceptionWhenLoadingNonExistingKeystore() {
227         assertThatThrownBy(() -> KeyStoreUtils.loadKeyStore(KEYSTORE_LOCATION + NON_EXISTING_KEYSTORE_FILE_NAME, KEYSTORE_PASSWORD))
228                   .isInstanceOf(GenericKeyStoreException.class)
229                   .hasMessageContaining("KeyStore is not present for the giving input");
230     }
231 
232     @Test
233     void throwGenericKeyStoreExceptionWhenFailingToCloseStreamProperly() throws IOException {
234         String keystoreType = KeyStore.getDefaultType();
235         Path keystorePath = Paths.get(TEST_RESOURCES_LOCATION + KEYSTORE_LOCATION + IDENTITY_FILE_NAME).toAbsolutePath();
236         InputStream inputStream = spy(Files.newInputStream(keystorePath, StandardOpenOption.READ));
237 
238         try (MockedStatic<Files> filesMockedStatic = mockStatic(Files.class, invocation -> {
239             Method method = invocation.getMethod();
240             if ("newInputStream".equals(method.getName())) {
241                 return invocation.getMock();
242             } else {
243                 return invocation.callRealMethod();
244             }
245         })) {
246 
247             doThrow(new IOException("Could not close the stream")).when(inputStream).close();
248 
249             filesMockedStatic.when(() -> Files.newInputStream(any(Path.class), any(OpenOption.class))).thenReturn(inputStream);
250 
251             assertThatThrownBy(() -> KeyStoreUtils.loadKeyStore(keystorePath, KEYSTORE_PASSWORD, keystoreType))
252                     .isInstanceOf(GenericKeyStoreException.class)
253                     .hasMessageContaining("Could not close the stream");
254         }
255     }
256 
257     @Test
258     void throwExceptionWhenCreateKeyStoreForUnknownType() {
259         assertThatThrownBy(() -> KeyStoreUtils.createKeyStore("unknown", KEYSTORE_PASSWORD))
260                 .isInstanceOf(GenericKeyStoreException.class)
261                 .hasMessageContaining("unknown not found");
262     }
263 
264     @Test
265     void throwGenericKeyStoreWhenSetCertificateEntryThrowsKeyStoreException() throws KeyStoreException {
266         SSLFactory sslFactory = SSLFactory.builder()
267                 .withTrustMaterial(KEYSTORE_LOCATION + TRUSTSTORE_FILE_NAME, TRUSTSTORE_PASSWORD)
268                 .withDefaultTrustMaterial()
269                 .build();
270 
271         X509Certificate[] trustedCertificates = sslFactory.getTrustedCertificates().toArray(new X509Certificate[0]);
272 
273         KeyStore keyStore = mock(KeyStore.class);
274         doThrow(new KeyStoreException("lazy")).when(keyStore).setCertificateEntry(anyString(), any(Certificate.class));
275 
276         try (MockedStatic<KeyStoreUtils> keyStoreUtilsMock = mockStatic(KeyStoreUtils.class, invocation -> {
277             Method method = invocation.getMethod();
278             if ("createKeyStore".equals(method.getName()) && method.getParameterCount() == 0) {
279                 return invocation.getMock();
280             } else {
281                 return invocation.callRealMethod();
282             }
283         })) {
284             keyStoreUtilsMock.when(KeyStoreUtils::createKeyStore).thenReturn(keyStore);
285 
286             assertThatThrownBy(() -> KeyStoreUtils.createTrustStore(trustedCertificates))
287                     .isInstanceOf(GenericKeyStoreException.class)
288                     .hasMessageContaining("lazy");
289         }
290     }
291 
292     private void resetOsName() {
293         System.setProperty("os.name", ORIGINAL_OS_NAME);
294     }
295 
296     private InputStream getResource(String path) {
297         return this.getClass().getClassLoader().getResourceAsStream(path);
298     }
299 
300 }