2323import io .micrometer .core .instrument .MeterRegistry ;
2424import io .micrometer .core .instrument .Metrics ;
2525import io .micrometer .core .instrument .Timer ;
26+ import java .io .BufferedInputStream ;
27+ import java .io .ByteArrayInputStream ;
2628import java .io .FileNotFoundException ;
2729import java .io .InputStream ;
2830import java .net .*;
2931import java .net .http .HttpRequest ;
3032import java .net .http .HttpResponse ;
3133import java .nio .charset .StandardCharsets ;
34+ import java .nio .file .Files ;
3235import java .nio .file .Path ;
36+ import java .security .KeyStore ;
3337import java .security .SecureRandom ;
38+ import java .security .cert .Certificate ;
39+ import java .security .cert .CertificateFactory ;
3440import java .security .cert .X509Certificate ;
3541import java .time .Duration ;
3642import java .time .ZonedDateTime ;
43+ import java .util .Collection ;
3744import java .util .HashMap ;
3845import java .util .List ;
3946import java .util .Map ;
4653import javax .net .ssl .SSLContext ;
4754import javax .net .ssl .SSLEngine ;
4855import javax .net .ssl .TrustManager ;
56+ import javax .net .ssl .TrustManagerFactory ;
4957import javax .net .ssl .X509ExtendedTrustManager ;
5058import land .oras .ContainerRef ;
5159import 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