1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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 }