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.ssl.exception.GenericCertificateException;
20  import nl.altindag.ssl.exception.GenericIOException;
21  import org.junit.jupiter.api.Test;
22  import org.junit.jupiter.api.extension.ExtendWith;
23  import org.mockito.MockedStatic;
24  import org.mockito.invocation.InvocationOnMock;
25  import org.mockito.junit.jupiter.MockitoExtension;
26  import sun.security.util.ObjectIdentifier;
27  import sun.security.x509.BasicConstraintsExtension;
28  import sun.security.x509.X509CertImpl;
29  
30  import javax.net.ssl.X509ExtendedTrustManager;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.UncheckedIOException;
34  import java.lang.reflect.Method;
35  import java.net.URI;
36  import java.nio.file.Files;
37  import java.nio.file.Path;
38  import java.nio.file.Paths;
39  import java.security.KeyStore;
40  import java.security.KeyStoreException;
41  import java.security.cert.Certificate;
42  import java.security.cert.CertificateEncodingException;
43  import java.security.cert.CertificateException;
44  import java.security.cert.CertificateFactory;
45  import java.security.cert.X509Certificate;
46  import java.util.Collections;
47  import java.util.HashSet;
48  import java.util.List;
49  import java.util.Map;
50  import java.util.Objects;
51  import java.util.Set;
52  
53  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
54  import static nl.altindag.ssl.TestConstants.KEYSTORE_LOCATION;
55  import static nl.altindag.ssl.TestConstants.TRUSTSTORE_FILE_NAME;
56  import static nl.altindag.ssl.TestConstants.TRUSTSTORE_PASSWORD;
57  import static org.assertj.core.api.Assertions.assertThat;
58  import static org.assertj.core.api.Assertions.assertThatThrownBy;
59  import static org.mockito.ArgumentMatchers.any;
60  import static org.mockito.ArgumentMatchers.anyString;
61  import static org.mockito.Mockito.doThrow;
62  import static org.mockito.Mockito.mock;
63  import static org.mockito.Mockito.mockStatic;
64  import static org.mockito.Mockito.spy;
65  import static org.mockito.Mockito.times;
66  import static org.mockito.Mockito.when;
67  
68  /**
69   * @author Hakan Altindag
70   */
71  @ExtendWith(MockitoExtension.class)
72  class CertificateUtilsShould {
73  
74      private static final String PEM_LOCATION = "pems-for-unit-tests/";
75      private static final String TEMPORALLY_PEM_LOCATION = System.getProperty("user.home");
76  
77      @Test
78      void generateAliasForX509Certificate() {
79          X509ExtendedTrustManager trustManager = TrustManagerUtils.createTrustManager(KeyStoreUtils.loadKeyStore(KEYSTORE_LOCATION + TRUSTSTORE_FILE_NAME, TRUSTSTORE_PASSWORD));
80          X509Certificate certificate = trustManager.getAcceptedIssuers()[0];
81  
82          String alias = CertificateUtils.generateAlias(certificate);
83          assertThat(alias).isEqualTo("CN=*.google.com,O=Google LLC,L=Mountain View,ST=California,C=US");
84      }
85  
86      @Test
87      void generateAliasForCertificate() {
88          Certificate certificate = mock(Certificate.class);
89  
90          String alias = CertificateUtils.generateAlias(certificate);
91          assertThat(alias).isNotBlank();
92      }
93  
94      @Test
95      void loadCertificateFromClassPath() {
96          List<Certificate> certificates = CertificateUtils.loadCertificate(PEM_LOCATION + "badssl-certificate.pem");
97          assertThat(certificates).hasSize(1);
98      }
99  
100     @Test
101     void loadMultipleCertificatesFromDifferentFiles() {
102         List<Certificate> certificates = CertificateUtils.loadCertificate(
103                 PEM_LOCATION + "badssl-certificate.pem",
104                 PEM_LOCATION + "github-certificate.pem",
105                 PEM_LOCATION + "stackexchange.pem"
106         );
107         assertThat(certificates).hasSize(3);
108     }
109 
110     @Test
111     void loadCertificateFromDirectory() throws IOException {
112         Path certificatePath = copyFileToHomeDirectory(PEM_LOCATION, "github-certificate.pem");
113         List<Certificate> certificates = CertificateUtils.loadCertificate(certificatePath);
114 
115         assertThat(certificates).hasSize(1);
116 
117         Files.delete(certificatePath);
118     }
119 
120     @Test
121     void throwExceptionWhenLoadingCertificateFromUnknownPath() {
122         Path certificatePath = Paths.get("somewhere-in-space.pem");
123         assertThatThrownBy(() -> CertificateUtils.loadCertificate(certificatePath))
124                 .isInstanceOf(GenericIOException.class)
125                 .hasMessageContaining("java.nio.file.NoSuchFileException: somewhere-in-space.pem");
126     }
127 
128     @Test
129     void loadCertificateFromInputStream() throws IOException {
130         List<Certificate> certificates;
131         try(InputStream inputStream = getResource(PEM_LOCATION + "multiple-certificates.pem")) {
132             certificates = CertificateUtils.loadCertificate(inputStream);
133         }
134 
135         assertThat(certificates).hasSize(3);
136     }
137 
138     @Test
139     void getSystemTrustedCertificates() {
140         String operatingSystem = System.getProperty("os.name").toLowerCase();
141         List<X509Certificate> certificates = CertificateUtils.getSystemTrustedCertificates();
142         if (operatingSystem.contains("mac") || operatingSystem.contains("windows")) {
143             assertThat(certificates).isNotEmpty();
144         }
145 
146         if (operatingSystem.contains("linux")) {
147             assertThat(certificates).isEmpty();
148         }
149     }
150 
151     @Test
152     void getSystemTrustedCertificatesDoesNotReturnCertificateIfNotACertificateEntry() throws KeyStoreException {
153         KeyStore keyStore = mock(KeyStore.class);
154         try (MockedStatic<KeyStoreUtils> keyStoreUtilsMockedStatic = mockStatic(KeyStoreUtils.class, InvocationOnMock::getMock)) {
155 
156             keyStoreUtilsMockedStatic.when(KeyStoreUtils::loadSystemKeyStores).thenReturn(Collections.singletonList(keyStore));
157             when(keyStore.aliases()).thenReturn(Collections.enumeration(Collections.singletonList("client")));
158             when(keyStore.isCertificateEntry("client")).thenReturn(false);
159 
160             List<X509Certificate> certificates = CertificateUtils.getSystemTrustedCertificates();
161 
162             assertThat(certificates).isEmpty();
163         }
164     }
165 
166     @Test
167     void getCertificatesReturnsEmptyWhenNonHttpsUrlIsProvided() {
168         Map<String, List<Certificate>> certificates = CertificateUtils.getCertificate("http://www.google.com/");
169 
170         assertThat(certificates).containsKey("http://www.google.com/");
171         assertThat(certificates.get("http://www.google.com/")).isEmpty();
172     }
173 
174     @Test
175     void notAddSubjectAndIssuerAsHeaderWhenCertificateTypeIsNotX509Certificate() throws CertificateEncodingException {
176         Certificate certificate = mock(Certificate.class);
177 
178         when(certificate.getEncoded()).thenReturn(CertificateUtils.loadCertificate(PEM_LOCATION + "stackexchange.pem").get(0).getEncoded());
179 
180         String pem = CertificateUtils.convertToPem(certificate);
181         assertThat(pem)
182                 .doesNotContain("subject", "issuer")
183                 .contains("-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
184     }
185 
186     @Test
187     void getJdkTrustedCertificates() {
188         List<X509Certificate> jdkTrustedCertificates = CertificateUtils.getJdkTrustedCertificates();
189 
190         assertThat(jdkTrustedCertificates).hasSizeGreaterThan(0);
191     }
192 
193     @Test
194     void getRootCaIfPossibleReturnsJdkTrustedCaCertificateWhenNoAuthorityInfoAccessExtensionIsPresent() {
195         List<Certificate> certificates = CertificateUtils.getCertificate("https://www.reddit.com/")
196                 .get("https://www.reddit.com/");
197 
198         try (MockedStatic<CertificateUtils> certificateUtilsMockedStatic = mockStatic(CertificateUtils.class, invocation -> {
199             Method method = invocation.getMethod();
200             if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName())) {
201                 return invocation.getMock();
202             } else {
203                 return invocation.callRealMethod();
204             }
205         })) {
206             certificateUtilsMockedStatic.when(() -> CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(any(X509Certificate.class)))
207                     .thenReturn(Collections.emptyList());
208 
209             X509Certificate certificate = (X509Certificate) certificates.get(certificates.size() - 1);
210             List<X509Certificate> rootCaCertificate = CertificateUtils.getRootCaIfPossible(certificate);
211             assertThat(rootCaCertificate).isNotEmpty();
212 
213             certificateUtilsMockedStatic.verify(() -> CertificateUtils.getRootCaFromJdkTrustedCertificates(certificate), times(1));
214         }
215     }
216 
217     @Test
218     void getRootCaIfPossibleReturnsEmptyListWhenNoAuthorityInfoAccessExtensionIsPresentAndNoMatching() {
219         List<Certificate> certificates = CertificateUtils.getCertificate("https://www.reddit.com/")
220                 .get("https://www.reddit.com/");
221 
222         try (MockedStatic<CertificateUtils> certificateUtilsMockedStatic = mockStatic(CertificateUtils.class, invocation -> {
223             Method method = invocation.getMethod();
224             if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName()) || "getRootCaFromJdkTrustedCertificates".equals(method.getName())) {
225                 return invocation.getMock();
226             } else {
227                 return invocation.callRealMethod();
228             }
229         })) {
230             certificateUtilsMockedStatic.when(() -> CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(any(X509Certificate.class)))
231                     .thenReturn(Collections.emptyList());
232 
233             certificateUtilsMockedStatic.when(() -> CertificateUtils.getRootCaFromJdkTrustedCertificates(any(X509Certificate.class)))
234                     .thenReturn(Collections.emptyList());
235 
236             X509Certificate certificate = (X509Certificate) certificates.get(certificates.size() - 1);
237             List<X509Certificate> rootCaCertificate = CertificateUtils.getRootCaIfPossible(certificate);
238             assertThat(rootCaCertificate).isEmpty();
239 
240             certificateUtilsMockedStatic.verify(() -> CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(certificate), times(1));
241             certificateUtilsMockedStatic.verify(() -> CertificateUtils.getRootCaFromJdkTrustedCertificates(certificate), times(1));
242         }
243     }
244 
245     @Test
246     void getRootCaFromChainIfPossibleReturnsEmptyListWhenNoCertificatesHaveBeenProvided() {
247         List<X509Certificate> rootCa = CertificateUtils.getRootCaFromChainIfPossible(new Certificate[]{});
248         assertThat(rootCa).isEmpty();
249     }
250 
251     @Test
252     void getRootCaFromChainIfPossibleReturnsEmptyListWhenProvidedCertificateIsNotAnInstanceOfX509Certificate() {
253         List<X509Certificate> rootCa = CertificateUtils.getRootCaFromChainIfPossible(new Certificate[]{mock(Certificate.class)});
254         assertThat(rootCa).isEmpty();
255     }
256 
257     @Test
258     void getRootCaFromAuthorityInfoAccessExtensionIfPresentReturnsEmptyListWhenCertificateIsNotInstanceOfX509CertImpl() {
259         List<X509Certificate> rootCa = CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(mock(X509Certificate.class));
260         assertThat(rootCa).isEmpty();
261     }
262 
263     @Test
264     void getRootCaFromAuthorityInfoAccessExtensionIfPresentReturnsEmptyListWhenCertificateDoesNotContainNonCriticalExtensionOIDs() {
265         X509CertImpl certificate = mock(X509CertImpl.class);
266         when(certificate.getNonCriticalExtensionOIDs()).thenReturn(Collections.emptySet());
267 
268         List<X509Certificate> rootCa = CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(certificate);
269         assertThat(rootCa).isEmpty();
270     }
271 
272     @Test
273     void getRootCaFromAuthorityInfoAccessExtensionIfPresentReturnsEmptyListWhenCertificateExtensionDoesNotHaveAuthorityInfoAccessExtension() {
274         X509CertImpl certificate = mock(X509CertImpl.class);
275 
276         Set<String> extensionIds = new HashSet<>();
277         extensionIds.add("1.3.6.1.5.5.7.48.1");
278 
279         when(certificate.getNonCriticalExtensionOIDs()).thenReturn(extensionIds);
280         when(certificate.getExtension(any(ObjectIdentifier.class))).thenReturn(mock(BasicConstraintsExtension.class));
281 
282         List<X509Certificate> rootCa = CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(certificate);
283         assertThat(rootCa).isEmpty();
284     }
285 
286     @Test
287     void ThrowsGenericCertificateExceptionWhenGetCertificatesFromRemoteFileFails() {
288         try (MockedStatic<CertificateFactory> certificateFactoryMockedStatic = mockStatic(CertificateFactory.class, invocation -> {
289             Method method = invocation.getMethod();
290             if ("getInstance".equals(method.getName())) {
291                 return invocation.getMock();
292             } else {
293                 return invocation.callRealMethod();
294             }
295         })) {
296 
297             certificateFactoryMockedStatic.when(() -> CertificateFactory.getInstance("X.509"))
298                     .thenThrow(CertificateException.class);
299 
300             URI uri = URI.create("https://www.reddit.com/");
301             assertThatThrownBy(() -> CertificateUtils.getCertificatesFromRemoteFile(uri, null))
302                     .isInstanceOf(GenericCertificateException.class);
303         }
304     }
305 
306     @Test
307     void throwsGenericCertificateExceptionWhenGettingCertificateEncodingException() throws CertificateEncodingException {
308         Certificate certificate = mock(Certificate.class);
309 
310         doThrow(new CertificateEncodingException("KABOOM!")).when(certificate).getEncoded();
311 
312         assertThatThrownBy(() -> CertificateUtils.convertToPem(certificate))
313                 .isInstanceOf(GenericCertificateException.class)
314                 .hasMessage("java.security.cert.CertificateEncodingException: KABOOM!");
315     }
316 
317     @Test
318     void throwsUncheckedIOExceptionWhenUrlIsUnreachable() {
319         assertThatThrownBy(() -> CertificateUtils.getCertificate("https://localhost:1234/"))
320                 .isInstanceOf(UncheckedIOException.class);
321     }
322 
323     @Test
324     void throwsGenericIOExceptionWhenCloseOfTheStreamFails() throws IOException {
325         InputStream inputStream = spy(getResource(PEM_LOCATION + "multiple-certificates.pem"));
326 
327         doThrow(new IOException("Could not read the content")).when(inputStream).close();
328 
329         assertThatThrownBy(() -> CertificateUtils.loadCertificate(inputStream))
330                 .isInstanceOf(GenericIOException.class)
331                 .hasRootCauseMessage("Could not read the content");
332     }
333 
334     @Test
335     void throwsGenericCertificateExceptionWhenParsingNonSupportedCertificate() {
336         assertThatThrownBy(() -> CertificateUtils.parseCertificate("test"))
337                 .isInstanceOf(GenericCertificateException.class)
338                 .hasMessage(
339                         "There are no valid certificates present to parse. " +
340                         "Please make sure to supply at least one valid pem formatted " +
341                         "certificate containing the header -----BEGIN CERTIFICATE----- " +
342                         "and the footer -----END CERTIFICATE-----"
343                 );
344     }
345 
346     @Test
347     void throwsGenericCertificateExceptionWhenParseCertificateFails() throws CertificateException {
348         try (MockedStatic<CertificateFactory> certificateFactoryMockedStatic = mockStatic(CertificateFactory.class, InvocationOnMock::getMock)) {
349             CertificateFactory certificateFactory = mock(CertificateFactory.class);
350             when(certificateFactory.generateCertificate(any(InputStream.class))).thenThrow(new CertificateException("KABOOM!!!"));
351             certificateFactoryMockedStatic.when(() -> CertificateFactory.getInstance(anyString())).thenReturn(certificateFactory);
352 
353             InputStream resource = getResource(PEM_LOCATION + "github-certificate.pem");
354             String content = IOUtils.getContent(resource);
355 
356             assertThatThrownBy(() -> CertificateUtils.parseCertificate(content))
357                     .isInstanceOf(GenericCertificateException.class)
358                     .hasMessageContaining("KABOOM!!!");
359         }
360     }
361 
362     private Path copyFileToHomeDirectory(String path, String fileName) throws IOException {
363         try (InputStream file = Thread.currentThread().getContextClassLoader().getResourceAsStream(path + fileName)) {
364             Path destination = Paths.get(TEMPORALLY_PEM_LOCATION, fileName);
365             Files.copy(Objects.requireNonNull(file), destination, REPLACE_EXISTING);
366             return destination;
367         }
368     }
369 
370     private InputStream getResource(String path) {
371         return this.getClass().getClassLoader().getResourceAsStream(path);
372     }
373 
374 }