Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to disable resolving root ca #430

Merged
merged 6 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,29 @@
/**
* @author Hakan Altindag
*/
class CertificateExtractorUtils {
public class CertificateExtractingClient {

private static final Pattern CA_ISSUERS_AUTHORITY_INFO_ACCESS = Pattern.compile("(?s)^AuthorityInfoAccess\\h+\\[\\R\\s*\\[\\R.*?accessMethod:\\h+caIssuers\\R\\h*accessLocation: URIName:\\h+(https?://\\S+)", Pattern.MULTILINE);

private static CertificateExtractorUtils instance;
private static CertificateExtractingClient instance;

private final boolean shouldResolveRootCa;
private final Proxy proxy;
private final SSLFactory sslFactoryForCertificateCapturing;
private final SSLFactory unsafeSslFactory;
private final SSLSocketFactory unsafeSslSocketFactory;
private final SSLSocketFactory certificateCapturingSslSocketFactory;
private final List<X509Certificate> certificatesCollector;

private Proxy proxy;
private CertificateExtractingClient(boolean shouldResolveRootCa, Proxy proxy, PasswordAuthentication passwordAuthentication) {
this.shouldResolveRootCa = shouldResolveRootCa;
this.proxy = proxy;

if (passwordAuthentication != null) {
Authenticator authenticator = new FelixAuthenticator(passwordAuthentication);
Authenticator.setDefault(authenticator);
}

private CertificateExtractorUtils() {
certificatesCollector = new CopyOnWriteArrayList<>();

X509ExtendedTrustManager certificateCapturingTrustManager = TrustManagerUtils.createCertificateCapturingTrustManager(certificatesCollector);
Expand All @@ -80,28 +88,17 @@ private CertificateExtractorUtils() {
unsafeSslSocketFactory = unsafeSslFactory.getSslSocketFactory();
}

protected CertificateExtractorUtils(Proxy proxy) {
this();
this.proxy = proxy;
}

protected CertificateExtractorUtils(Proxy proxy, PasswordAuthentication passwordAuthentication) {
this(proxy);
Authenticator authenticator = new FelixAuthenticator(passwordAuthentication);
Authenticator.setDefault(authenticator);
}

static CertificateExtractorUtils getInstance() {
static CertificateExtractingClient getInstance() {
if (instance == null) {
instance = new CertificateExtractorUtils();
instance = new CertificateExtractingClient(true, null, null);
} else {
instance.certificatesCollector.clear();
SSLSessionUtils.invalidateCaches(instance.sslFactoryForCertificateCapturing);
}
return instance;
}

List<X509Certificate> getCertificateFromExternalSource(String url) {
public List<X509Certificate> get(String url) {
try {
URL parsedUrl = new URL(url);
if ("https".equalsIgnoreCase(parsedUrl.getProtocol())) {
Expand All @@ -110,10 +107,14 @@ List<X509Certificate> getCertificateFromExternalSource(String url) {
connection.connect();
connection.disconnect();

List<X509Certificate> rootCa = getRootCaFromChainIfPossible(certificatesCollector);
return Stream.of(certificatesCollector, rootCa)
.flatMap(Collection::stream)
.collect(toUnmodifiableList());
if (shouldResolveRootCa) {
List<X509Certificate> resolvedRootCa = getRootCaFromChainIfPossible(certificatesCollector);
return Stream.of(certificatesCollector, resolvedRootCa)
.flatMap(Collection::stream)
.collect(toUnmodifiableList());
}

return Collections.unmodifiableList(certificatesCollector);
} else {
return Collections.emptyList();
}
Expand Down Expand Up @@ -223,4 +224,35 @@ protected PasswordAuthentication getPasswordAuthentication() {
}
}

public static Builder builder() {
return new Builder();
}

public static class Builder {

private Proxy proxy = null;
private PasswordAuthentication passwordAuthentication = null;
private boolean shouldResolveRootCa = true;

public Builder withProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}

public Builder withProxyPasswordAuthentication(PasswordAuthentication passwordAuthentication) {
this.passwordAuthentication = passwordAuthentication;
return this;
}

public Builder withResolvedRootCa(boolean shouldResolveRootCa) {
this.shouldResolveRootCa = shouldResolveRootCa;
return this;
}

public CertificateExtractingClient build() {
return new CertificateExtractingClient(shouldResolveRootCa, proxy, passwordAuthentication);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,24 @@ public static List<X509Certificate> getSystemTrustedCertificates() {
}

public static List<X509Certificate> getCertificatesFromExternalSource(String url) {
return CertificateExtractorUtils.getInstance().getCertificateFromExternalSource(url);
return CertificateExtractingClient.getInstance().get(url);
}

public static List<X509Certificate> getCertificatesFromExternalSource(Proxy proxy, String url) {
return new CertificateExtractorUtils(proxy).getCertificateFromExternalSource(url);
return CertificateExtractingClient.builder()
.withResolvedRootCa(true)
.withProxy(proxy)
.build()
.get(url);
}

public static List<X509Certificate> getCertificatesFromExternalSource(Proxy proxy, PasswordAuthentication passwordAuthentication, String url) {
return new CertificateExtractorUtils(proxy, passwordAuthentication).getCertificateFromExternalSource(url);
return CertificateExtractingClient.builder()
.withResolvedRootCa(true)
.withProxy(proxy)
.withProxyPasswordAuthentication(passwordAuthentication)
.build()
.get(url);
}

public static List<String> getCertificatesFromExternalSourceAsPem(String url) {
Expand Down Expand Up @@ -324,20 +333,27 @@ public static Map<String, List<X509Certificate>> getCertificatesFromExternalSour
}

public static Map<String, List<X509Certificate>> getCertificatesFromExternalSources(Proxy proxy, List<String> urls) {
CertificateExtractorUtils certificateExtractorUtils = new CertificateExtractorUtils(proxy);
CertificateExtractingClient client = CertificateExtractingClient.builder()
.withResolvedRootCa(true)
.withProxy(proxy)
.build();

return urls.stream()
.distinct()
.map(url -> new AbstractMap.SimpleEntry<>(url, certificateExtractorUtils.getCertificateFromExternalSource(url)))
.map(url -> new AbstractMap.SimpleEntry<>(url, client.get(url)))
.collect(Collectors.collectingAndThen(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (key1, key2) -> key1, LinkedHashMap::new), Collections::unmodifiableMap));
}

public static Map<String, List<X509Certificate>> getCertificatesFromExternalSources(Proxy proxy, PasswordAuthentication passwordAuthentication, List<String> urls) {
CertificateExtractorUtils certificateExtractorUtils = new CertificateExtractorUtils(proxy, passwordAuthentication);
CertificateExtractingClient client = CertificateExtractingClient.builder()
.withResolvedRootCa(true)
.withProxyPasswordAuthentication(passwordAuthentication)
.withProxy(proxy)
.build();

return urls.stream()
.distinct()
.map(url -> new AbstractMap.SimpleEntry<>(url, certificateExtractorUtils.getCertificateFromExternalSource(url)))
.map(url -> new AbstractMap.SimpleEntry<>(url, client.get(url)))
.collect(Collectors.collectingAndThen(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (key1, key2) -> key1, LinkedHashMap::new), Collections::unmodifiableMap));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
Expand All @@ -58,21 +59,21 @@
* @author Hakan Altindag
*/
@ExtendWith(MockitoExtension.class)
class CertificateExtractorUtilsShould {
class CertificateExtractingClientShould {

@Test
void getRootCaIfPossibleReturnsJdkTrustedCaCertificateWhenNoAuthorityInfoAccessExtensionIsPresent() {
List<X509Certificate> certificates = CertificateUtils.getCertificatesFromExternalSource("https://www.reddit.com/");

try (MockedStatic<CertificateExtractorUtils> mockedStatic = mockStatic(CertificateExtractorUtils.class, invocation -> {
try (MockedStatic<CertificateExtractingClient> mockedStatic = mockStatic(CertificateExtractingClient.class, invocation -> {
Method method = invocation.getMethod();
if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName())) {
return Collections.emptyList();
} else {
return invocation.callRealMethod();
}
})) {
CertificateExtractorUtils victim = spy(CertificateExtractorUtils.getInstance());
CertificateExtractingClient victim = spy(CertificateExtractingClient.getInstance());

X509Certificate certificate = certificates.get(certificates.size() - 1);
List<X509Certificate> rootCaCertificate = victim.getRootCaIfPossible(certificate);
Expand All @@ -86,15 +87,15 @@ void getRootCaIfPossibleReturnsJdkTrustedCaCertificateWhenNoAuthorityInfoAccessE
void getRootCaIfPossibleReturnsEmptyListWhenNoAuthorityInfoAccessExtensionIsPresentAndNoMatching() {
List<X509Certificate> certificates = CertificateUtils.getCertificatesFromExternalSource("https://www.reddit.com/");

try (MockedStatic<CertificateExtractorUtils> mockedStatic = mockStatic(CertificateExtractorUtils.class, invocation -> {
try (MockedStatic<CertificateExtractingClient> mockedStatic = mockStatic(CertificateExtractingClient.class, invocation -> {
Method method = invocation.getMethod();
if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName()) || "getRootCaFromJdkTrustedCertificates".equals(method.getName())) {
return Collections.emptyList();
} else {
return invocation.callRealMethod();
}
})) {
CertificateExtractorUtils victim = spy(CertificateExtractorUtils.getInstance());
CertificateExtractingClient victim = spy(CertificateExtractingClient.getInstance());

doReturn(Collections.emptyList())
.when(victim)
Expand All @@ -109,21 +110,32 @@ void getRootCaIfPossibleReturnsEmptyListWhenNoAuthorityInfoAccessExtensionIsPres
}
}

@Test
void rootCaIsNotResolvedWhenDisabled() {
CertificateExtractingClient client = spy(CertificateExtractingClient.builder()
.withResolvedRootCa(false)
.build());

client.get("https://www.reddit.com/");

verify(client, times(0)).getRootCaFromChainIfPossible(anyList());
}

@Test
void getRootCaFromChainIfPossibleReturnsEmptyListWhenNoCertificatesHaveBeenProvided() {
List<X509Certificate> rootCa = CertificateExtractorUtils.getInstance().getRootCaFromChainIfPossible(Collections.emptyList());
List<X509Certificate> rootCa = CertificateExtractingClient.getInstance().getRootCaFromChainIfPossible(Collections.emptyList());
assertThat(rootCa).isEmpty();
}

@Test
void getRootCaFromAuthorityInfoAccessExtensionIfPresentReturnsEmptyListWhenCertificateIsNotInstanceOfX509CertImpl() {
List<X509Certificate> rootCa = CertificateExtractorUtils.getInstance().getRootCaFromAuthorityInfoAccessExtensionIfPresent(mock(X509Certificate.class));
List<X509Certificate> rootCa = CertificateExtractingClient.getInstance().getRootCaFromAuthorityInfoAccessExtensionIfPresent(mock(X509Certificate.class));
assertThat(rootCa).isEmpty();
}

@Test
void throwsGenericCertificateExceptionWhenGetCertificatesFromRemoteFileFails() throws MalformedURLException {
CertificateExtractorUtils victim = CertificateExtractorUtils.getInstance();
CertificateExtractingClient victim = CertificateExtractingClient.getInstance();

URI uri = mock(URI.class);
doThrow(new MalformedURLException("KABOOM!!!"))
Expand All @@ -140,20 +152,23 @@ void reUseExistingUnsafeSslSocketFactory() throws CertificateException, NoSuchAl
X509Certificate intermediateCertificate = mock(X509Certificate.class);
doNothing().when(intermediateCertificate).verify(any());

List<X509Certificate> certificatesFromRemoteFile = CertificateExtractorUtils.getInstance().getCertificatesFromRemoteFile(uri, intermediateCertificate);
List<X509Certificate> certificatesFromRemoteFile = CertificateExtractingClient.getInstance().getCertificatesFromRemoteFile(uri, intermediateCertificate);
assertThat(certificatesFromRemoteFile).isNotEmpty();
}

@Test
void extractCertificatesWithProxyAndAuthentication() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
CertificateExtractorUtils certificateExtractorUtilsWithoutProxy = CertificateExtractorUtils.getInstance();
List<X509Certificate> certificates = certificateExtractorUtilsWithoutProxy.getCertificateFromExternalSource("https://google.com");
CertificateExtractingClient certificateExtractingClientWithoutProxy = CertificateExtractingClient.getInstance();
List<X509Certificate> certificates = certificateExtractingClientWithoutProxy.get("https://google.com");
assertThat(certificates).isNotEmpty();

Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("my-custom-host", 8081));
CertificateExtractorUtils certificateExtractorUtilsWithProxy = new CertificateExtractorUtils(proxy);
CertificateExtractingClient certificateExtractingClientWithProxy = CertificateExtractingClient.builder()
.withProxy(proxy)
.withResolvedRootCa(true)
.build();

assertThatThrownBy(() -> certificateExtractorUtilsWithProxy.getCertificateFromExternalSource("https://google.com"))
assertThatThrownBy(() -> certificateExtractingClientWithProxy.get("https://google.com"))
.isInstanceOf(GenericIOException.class)
.hasMessage("Failed getting certificate from: [https://google.com]")
.hasRootCauseInstanceOf(UnknownHostException.class)
Expand All @@ -163,9 +178,13 @@ void extractCertificatesWithProxyAndAuthentication() throws NoSuchMethodExceptio
ArgumentCaptor<Authenticator> authenticatorCaptor = ArgumentCaptor.forClass(Authenticator.class);

PasswordAuthentication passwordAuthentication = new PasswordAuthentication("foo", "bar".toCharArray());
CertificateExtractorUtils certificateExtractorUtilsWithProxyAndAuthentication = new CertificateExtractorUtils(proxy, passwordAuthentication);
CertificateExtractingClient certificateExtractingClientWithProxyAndAuthentication = CertificateExtractingClient.builder()
.withProxy(proxy)
.withProxyPasswordAuthentication(passwordAuthentication)
.withResolvedRootCa(true)
.build();

assertThatThrownBy(() -> certificateExtractorUtilsWithProxyAndAuthentication.getCertificateFromExternalSource("https://google.com"))
assertThatThrownBy(() -> certificateExtractingClientWithProxyAndAuthentication.get("https://google.com"))
.isInstanceOf(GenericIOException.class)
.hasMessage("Failed getting certificate from: [https://google.com]")
.hasRootCauseInstanceOf(UnknownHostException.class)
Expand Down
Loading
Loading