Skip to content

Commit ecda376

Browse files
authored
Support configuring custom CA certificates (#687)
Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
1 parent f230da5 commit ecda376

12 files changed

Lines changed: 859 additions & 126 deletions

File tree

pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@
155155
<artifactId>awaitility</artifactId>
156156
<version>${awaitability.version}</version>
157157
</dependency>
158+
<dependency>
159+
<groupId>org.bouncycastle</groupId>
160+
<artifactId>bcpkix-jdk18on</artifactId>
161+
<version>${bcprov-jdk18on.version}</version>
162+
</dependency>
158163
<dependency>
159164
<groupId>org.bouncycastle</groupId>
160165
<artifactId>bcprov-jdk18on</artifactId>
@@ -262,6 +267,11 @@
262267
<artifactId>awaitility</artifactId>
263268
<scope>test</scope>
264269
</dependency>
270+
<dependency>
271+
<groupId>org.bouncycastle</groupId>
272+
<artifactId>bcpkix-jdk18on</artifactId>
273+
<scope>test</scope>
274+
</dependency>
265275
<dependency>
266276
<groupId>org.junit.jupiter</groupId>
267277
<artifactId>junit-jupiter-api</artifactId>

src/main/java/land/oras/Registry.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ public final class Registry extends OCI<ContainerRef> {
102102
*/
103103
private boolean skipTlsVerify;
104104

105+
/**
106+
* Path to a PEM-encoded CA certificate or bundle for TLS verification
107+
*/
108+
private @Nullable Path caFilePath;
109+
110+
/**
111+
* PEM-encoded CA certificate or bundle content for TLS verification
112+
*/
113+
private @Nullable String caContent;
114+
105115
/**
106116
* The meter registry for metrics
107117
*/
@@ -250,6 +260,22 @@ private void setSkipTlsVerify(boolean skipTlsVerify) {
250260
this.skipTlsVerify = skipTlsVerify;
251261
}
252262

263+
/**
264+
* Set the CA file path for TLS verification
265+
* @param caFilePath The path to a PEM-encoded CA certificate or bundle
266+
*/
267+
private void setCaFilePath(Path caFilePath) {
268+
this.caFilePath = caFilePath;
269+
}
270+
271+
/**
272+
* Set the CA certificate content for TLS verification
273+
* @param caContent The PEM-encoded CA certificate or bundle content
274+
*/
275+
private void setCaContent(String caContent) {
276+
this.caContent = caContent;
277+
}
278+
253279
/**
254280
* Set the meter registry for metrics
255281
* @param meterRegistry The meter registry
@@ -264,6 +290,12 @@ private void setMeterRegistry(MeterRegistry meterRegistry) {
264290
*/
265291
private Registry build() {
266292
HttpClient.Builder clientBuilder = HttpClient.Builder.builder().withSkipTlsVerify(skipTlsVerify);
293+
if (caFilePath != null) {
294+
clientBuilder = clientBuilder.withCaFile(caFilePath);
295+
}
296+
if (caContent != null) {
297+
clientBuilder = clientBuilder.withCaContent(caContent);
298+
}
267299
if (meterRegistry != null) {
268300
clientBuilder = clientBuilder.withMeterRegistry(meterRegistry);
269301
}
@@ -1344,6 +1376,35 @@ public Builder withSkipTlsVerify(boolean skipTlsVerify) {
13441376
return this;
13451377
}
13461378

1379+
/**
1380+
* Set the CA file for TLS verification
1381+
* @param caFilePath The path to a PEM-encoded CA certificate or bundle
1382+
* @return The builder
1383+
*/
1384+
public Builder withCaFile(Path caFilePath) {
1385+
registry.setCaFilePath(caFilePath);
1386+
return this;
1387+
}
1388+
1389+
/**
1390+
* Set the CA file for TLS verification
1391+
* @param caFilePath The path to a PEM-encoded CA certificate or bundle
1392+
* @return The builder
1393+
*/
1394+
public Builder withCaFile(String caFilePath) {
1395+
return withCaFile(Path.of(caFilePath));
1396+
}
1397+
1398+
/**
1399+
* Set the CA certificates from PEM-encoded content
1400+
* @param caContent The PEM-encoded CA certificate or bundle content
1401+
* @return The builder
1402+
*/
1403+
public Builder withCaContent(String caContent) {
1404+
registry.setCaContent(caContent);
1405+
return this;
1406+
}
1407+
13471408
/**
13481409
* Set the meter registry for metrics. Following Micrometer best practices for libraries,
13491410
* a {@link SimpleMeterRegistry} is used by default when no registry is provided.

src/main/java/land/oras/auth/HttpClient.java

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,24 @@
2323
import io.micrometer.core.instrument.MeterRegistry;
2424
import io.micrometer.core.instrument.Metrics;
2525
import io.micrometer.core.instrument.Timer;
26+
import java.io.BufferedInputStream;
27+
import java.io.ByteArrayInputStream;
2628
import java.io.FileNotFoundException;
2729
import java.io.InputStream;
2830
import java.net.*;
2931
import java.net.http.HttpRequest;
3032
import java.net.http.HttpResponse;
3133
import java.nio.charset.StandardCharsets;
34+
import java.nio.file.Files;
3235
import java.nio.file.Path;
36+
import java.security.KeyStore;
3337
import java.security.SecureRandom;
38+
import java.security.cert.Certificate;
39+
import java.security.cert.CertificateFactory;
3440
import java.security.cert.X509Certificate;
3541
import java.time.Duration;
3642
import java.time.ZonedDateTime;
43+
import java.util.Collection;
3744
import java.util.HashMap;
3845
import java.util.List;
3946
import java.util.Map;
@@ -46,6 +53,7 @@
4653
import javax.net.ssl.SSLContext;
4754
import javax.net.ssl.SSLEngine;
4855
import javax.net.ssl.TrustManager;
56+
import javax.net.ssl.TrustManagerFactory;
4957
import javax.net.ssl.X509ExtendedTrustManager;
5058
import land.oras.ContainerRef;
5159
import land.oras.OrasModel;
@@ -90,6 +98,16 @@ public final class HttpClient {
9098
*/
9199
private boolean skipTlsVerify;
92100

101+
/**
102+
* Path to a PEM-encoded CA certificate or bundle
103+
*/
104+
private @Nullable Path caFilePath;
105+
106+
/**
107+
* PEM-encoded CA certificate or bundle content
108+
*/
109+
private @Nullable String caContent;
110+
93111
/**
94112
* Timeout in seconds
95113
*/
@@ -128,16 +146,93 @@ private void setTimeout(@Nullable Integer timeout) {
128146
* Skip the TLS verification
129147
* @param skipTlsVerify Skip TLS verification
130148
*/
131-
private void setTlsVerify(boolean skipTlsVerify) {
149+
private void setSkipTlsVerify(boolean skipTlsVerify) {
132150
this.skipTlsVerify = skipTlsVerify;
133-
if (skipTlsVerify) {
134-
try {
135-
SSLContext sslContext = SSLContext.getInstance("TLS");
136-
sslContext.init(null, new TrustManager[] {new InsecureTrustManager()}, new SecureRandom());
137-
builder.sslContext(sslContext);
138-
} catch (Exception e) {
139-
throw new OrasException("Unable to skip TLS verification", e);
140-
}
151+
}
152+
153+
/**
154+
* Set the CA certificates for TLS verification from a file
155+
* @param caFilePath The path to a PEM-encoded CA certificate or bundle
156+
*/
157+
private void setCaFile(Path caFilePath) {
158+
this.caFilePath = caFilePath;
159+
}
160+
161+
/**
162+
* Set the CA certificates for TLS verification from PEM-encoded content
163+
* @param caContent The PEM-encoded CA certificate or bundle content
164+
*/
165+
private void setCaContent(String caContent) {
166+
this.caContent = caContent;
167+
}
168+
169+
/**
170+
* Configure SSL context from a PEM-encoded CA file
171+
* @param caFilePath The path to a PEM-encoded CA certificate or bundle
172+
*/
173+
private void configureTlsFromFile(Path caFilePath) {
174+
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(caFilePath))) {
175+
configureCaCertificates(inputStream, "CA file: " + caFilePath);
176+
} catch (OrasException e) {
177+
throw e;
178+
} catch (Exception e) {
179+
throw new OrasException("Unable to configure CA file: " + caFilePath, e);
180+
}
181+
}
182+
183+
/**
184+
* Configure SSL context from PEM-encoded CA content
185+
* @param caContent The PEM-encoded CA certificate or bundle content
186+
*/
187+
private void configureTlsFromContent(String caContent) {
188+
try (InputStream inputStream = new ByteArrayInputStream(caContent.getBytes(StandardCharsets.UTF_8))) {
189+
configureCaCertificates(inputStream, "CA content");
190+
} catch (OrasException e) {
191+
throw e;
192+
} catch (Exception e) {
193+
throw new OrasException("Unable to configure CA certificates from content", e);
194+
}
195+
}
196+
197+
/**
198+
* Configure SSL context from PEM-encoded CA certificates read from the given input stream.
199+
* @param inputStream The input stream containing PEM-encoded certificates
200+
* @param source A description of the certificate source for error messages
201+
*/
202+
private void configureCaCertificates(InputStream inputStream, String source) throws Exception {
203+
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
204+
205+
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
206+
trustStore.load(null, null);
207+
208+
int certificateIndex = 0;
209+
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(inputStream);
210+
if (certificates.isEmpty()) {
211+
throw new OrasException("No certificates found in the provided " + source);
212+
}
213+
for (var certificate : certificates) {
214+
trustStore.setCertificateEntry("ca-" + certificateIndex++, certificate);
215+
}
216+
217+
TrustManagerFactory trustManagerFactory =
218+
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
219+
trustManagerFactory.init(trustStore);
220+
221+
SSLContext sslContext = SSLContext.getInstance("TLS");
222+
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
223+
builder.sslContext(sslContext);
224+
}
225+
226+
/**
227+
* Configure SSL context to skip TLS verification
228+
*/
229+
private void configureInsecureTls() {
230+
try {
231+
SSLContext sslContext = SSLContext.getInstance("TLS");
232+
sslContext.init(null, new TrustManager[] {new InsecureTrustManager()}, new SecureRandom());
233+
builder.sslContext(sslContext);
234+
} catch (Exception e) {
235+
throw new OrasException("Unable to skip TLS verification", e);
141236
}
142237
}
143238

@@ -146,6 +241,23 @@ private void setTlsVerify(boolean skipTlsVerify) {
146241
* @return The client
147242
*/
148243
public HttpClient build() {
244+
if (caFilePath != null && caContent != null) {
245+
throw new OrasException(
246+
"Cannot configure both a CA file and CA content. Use either withCaFile() or withCaContent(), not both");
247+
}
248+
if (skipTlsVerify && (caFilePath != null || caContent != null)) {
249+
throw new OrasException(
250+
"Cannot combine skipTlsVerify with a CA file or CA content. Use either withSkipTlsVerify() or withCaFile()/withCaContent(), not both");
251+
}
252+
253+
if (skipTlsVerify) {
254+
configureInsecureTls();
255+
} else if (caFilePath != null) {
256+
configureTlsFromFile(caFilePath);
257+
} else if (caContent != null) {
258+
configureTlsFromContent(caContent);
259+
}
260+
149261
this.client = this.builder.build();
150262
return this;
151263
}
@@ -778,7 +890,7 @@ public Builder withTimeout(@Nullable Integer timeout) {
778890
* @return The builder
779891
*/
780892
public Builder withSkipTlsVerify(boolean skipTlsVerify) {
781-
client.setTlsVerify(skipTlsVerify);
893+
client.setSkipTlsVerify(skipTlsVerify);
782894
return this;
783895
}
784896

@@ -792,6 +904,35 @@ public Builder withMeterRegistry(MeterRegistry meterRegistry) {
792904
return this;
793905
}
794906

907+
/**
908+
* Set the CA file for TLS verification
909+
* @param caFilePath The path to a PEM-encoded CA certificate or bundle
910+
* @return The builder
911+
*/
912+
public Builder withCaFile(Path caFilePath) {
913+
client.setCaFile(caFilePath);
914+
return this;
915+
}
916+
917+
/**
918+
* Set the CA file for TLS verification
919+
* @param caFilePath The path to a PEM-encoded CA certificate or bundle
920+
* @return The builder
921+
*/
922+
public Builder withCaFile(String caFilePath) {
923+
return withCaFile(Path.of(caFilePath));
924+
}
925+
926+
/**
927+
* Set the CA certificates from PEM-encoded content
928+
* @param caContent The PEM-encoded CA certificate or bundle content
929+
* @return The builder
930+
*/
931+
public Builder withCaContent(String caContent) {
932+
client.setCaContent(caContent);
933+
return this;
934+
}
935+
795936
/**
796937
* Build the client
797938
* @return The client

0 commit comments

Comments
 (0)