• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 25508088456

07 May 2026 04:17PM UTC coverage: 70.732% (+0.09%) from 70.647%
25508088456

Pull #1885

github

web-flow
Merge 1a283373a into fd215c395
Pull Request #1885: 1518 uv tools are now installed locally

4401 of 6878 branches covered (63.99%)

Branch coverage included in aggregate %.

11361 of 15406 relevant lines covered (73.74%)

3.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.22
cli/src/main/java/com/devonfw/tools/ide/util/TruststoreUtil.java
1
package com.devonfw.tools.ide.util;
2

3
import java.io.InputStream;
4
import java.io.OutputStream;
5
import java.net.InetSocketAddress;
6
import java.net.URI;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.security.KeyStore;
10
import java.security.SecureRandom;
11
import java.security.cert.Certificate;
12
import java.security.cert.CertificateException;
13
import java.security.cert.X509Certificate;
14
import java.util.Arrays;
15
import java.util.Enumeration;
16
import java.util.Locale;
17
import java.util.Objects;
18
import java.util.Set;
19
import javax.net.ssl.SSLContext;
20
import javax.net.ssl.SSLException;
21
import javax.net.ssl.SSLSocket;
22
import javax.net.ssl.SSLSocketFactory;
23
import javax.net.ssl.TrustManager;
24
import javax.net.ssl.TrustManagerFactory;
25
import javax.net.ssl.X509TrustManager;
26

27
/**
28
 * Utility methods for truststore handling and TLS certificate capture.
29
 */
30
public final class TruststoreUtil {
31

32
  /**
33
   * Parsed TLS endpoint with host and port.
34
   *
35
   * @param host the server host.
36
   * @param port the server port.
37
   */
38
  public record TlsEndpoint(String host, int port) {
9✔
39

40
  }
41

42
  private static final String TRUSTSTORE_PASSWORD = "changeit";
43

44
  /**
45
   * Default password for the JRE cacerts truststore
46
   */
47
  public static final String DEFAULT_CACERTS_PASSWORD = TRUSTSTORE_PASSWORD;
48

49
  /**
50
   * Password for the custom truststore
51
   */
52
  public static final String CUSTOM_TRUSTSTORE_PASSWORD = TRUSTSTORE_PASSWORD;
53

54
  /**
55
   * Default prefix for aliases of certificates added to the truststore.
56
   */
57
  private static final String DEFAULT_ALIAS_PREFIX = "custom";
58

59
  private static final int DEFAULT_TIMEOUT_MILLIS = 10_000;
60

61
  private static final String TLS_PROTOCOL = "TLS";
62

63
  private TruststoreUtil() {
64
    // utility class
65
  }
66

67
  /**
68
   * Checks if a truststore file exists at the specified path.
69
   *
70
   * @param path the path to the truststore file.
71
   * @return {@code true} if a truststore file exists at the specified path, {@code false} otherwise.
72
   */
73
  public static boolean isTruststorePresent(Path path) {
74
    return (path != null) && Files.exists(path);
11!
75
  }
76

77
  /**
78
   * Loads the default Java truststore from the JRE cacerts file.
79
   *
80
   * @return the default Java truststore loaded from the JRE cacerts file.
81
   * @throws Exception if an error occurs while loading the default truststore.
82
   */
83
  public static KeyStore getDefaultTruststore() throws Exception {
84
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
3✔
85
    trustManagerFactory.init((KeyStore) null);
4✔
86

87
    for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
15!
88
      if (trustManager instanceof X509TrustManager x509TrustManager) {
6!
89
        KeyStore truststore = KeyStore.getInstance(KeyStore.getDefaultType());
3✔
90
        truststore.load(null, DEFAULT_CACERTS_PASSWORD.toCharArray());
5✔
91

92
        int i = 0;
2✔
93
        for (X509Certificate cert : x509TrustManager.getAcceptedIssuers()) {
17✔
94
          truststore.setCertificateEntry("cert-" + i, cert);
5✔
95
          i++;
1✔
96
        }
97
        return truststore;
2✔
98
      }
99
    }
100
    throw new IllegalStateException("No X509TrustManager found in default TrustManagerFactory");
×
101
  }
102

103
  /**
104
   * Copies all certificate entries from the source truststore to the target truststore. Key entries are not copied, but if a key entry is encountered, its
105
   * first certificate in the chain is copied as a certificate entry.
106
   *
107
   * @param source the source truststore to copy from.
108
   * @param target the target truststore to copy to.
109
   * @throws Exception if an error occurs while copying the truststore.
110
   */
111
  public static void copyTruststore(KeyStore source, KeyStore target) throws Exception {
112
    Objects.requireNonNull(source, "source");
4✔
113
    Objects.requireNonNull(target, "target");
4✔
114

115
    Enumeration<String> aliases = source.aliases();
3✔
116
    while (aliases.hasMoreElements()) {
3✔
117
      String alias = aliases.nextElement();
4✔
118
      if (source.isCertificateEntry(alias)) {
4!
119
        Certificate cert = source.getCertificate(alias);
4✔
120
        if (cert != null) {
2!
121
          target.setCertificateEntry(alias, cert);
4✔
122
        }
123
      } else if (source.isKeyEntry(alias)) {
1!
124
        Certificate[] chain = source.getCertificateChain(alias);
×
125
        if ((chain != null) && (chain.length > 0)) {
×
126
          target.setCertificateEntry(alias, chain[0]);
×
127
        }
128
      }
129
    }
1✔
130
  }
1✔
131

132
  /**
133
   * Creates a new truststore at the specified path or updates an existing one by adding the given certificate if it is not already present. If the truststore
134
   * does not
135
   *
136
   * @param customTruststorePath the path to the custom truststore file to create or update.
137
   * @param certificate the certificate to add to the truststore if not already present.
138
   * @param aliasPrefix the prefix to use for the alias of the new certificate (e.g. "custom"). If {@code null} or blank, a default prefix is used.
139
   * @throws Exception if an error occurs while creating or updating the truststore.
140
   */
141
  public static void createOrUpdateTruststore(Path customTruststorePath, X509Certificate certificate, String aliasPrefix) throws Exception {
142
    Objects.requireNonNull(customTruststorePath, "customTruststorePath");
4✔
143
    Objects.requireNonNull(certificate, "certificate");
4✔
144

145
    if ((aliasPrefix == null) || aliasPrefix.isBlank()) {
5!
146
      aliasPrefix = DEFAULT_ALIAS_PREFIX;
×
147
    }
148

149
    Path parent = customTruststorePath.getParent();
3✔
150
    if (parent != null) {
2!
151
      Files.createDirectories(parent);
5✔
152
    }
153

154
    KeyStore customStore = KeyStore.getInstance("PKCS12");
3✔
155
    if (isTruststorePresent(customTruststorePath)) {
3✔
156
      try (InputStream in = Files.newInputStream(customTruststorePath)) {
6✔
157
        customStore.load(in, CUSTOM_TRUSTSTORE_PASSWORD.toCharArray());
5✔
158
      }
159
    } else {
160
      customStore.load(null, CUSTOM_TRUSTSTORE_PASSWORD.toCharArray());
5✔
161
      copyTruststore(getDefaultTruststore(), customStore);
3✔
162
    }
163

164
    if (!containsCertificate(customStore, certificate)) {
4✔
165
      String alias = makeUniqueAlias(customStore, aliasPrefix);
4✔
166
      addCertificate(customStore, alias, certificate);
4✔
167
    }
168

169
    try (OutputStream out = Files.newOutputStream(customTruststorePath)) {
5✔
170
      customStore.store(out, CUSTOM_TRUSTSTORE_PASSWORD.toCharArray());
5✔
171
    }
172
  }
1✔
173

174
  /**
175
   * Adds the given certificate to the truststore under the specified alias. If the alias already exists, it will be overwritten.
176
   *
177
   * @param truststore the truststore to add the certificate to.
178
   * @param alias the alias under which to add the certificate.
179
   * @param certificate the certificate to add to the truststore.
180
   * @throws Exception if an error occurs while adding the certificate to the truststore.
181
   */
182
  public static void addCertificate(KeyStore truststore, String alias, X509Certificate certificate) throws Exception {
183
    Objects.requireNonNull(truststore, "truststore");
4✔
184
    Objects.requireNonNull(alias, "alias");
4✔
185
    Objects.requireNonNull(certificate, "certificate");
4✔
186
    truststore.setCertificateEntry(alias, certificate);
4✔
187
  }
1✔
188

189
  /**
190
   * Parses a user input to a TLS endpoint supporting forms like {@code host}, {@code host:port}, and {@code https://host[:port]/path}.
191
   *
192
   * @param input the user input.
193
   * @return the parsed {@link TlsEndpoint}.
194
   */
195
  public static TlsEndpoint parseTlsEndpoint(String input) {
196
    if ((input == null) || input.isBlank()) {
5!
197
      throw new IllegalArgumentException("URL/host must not be empty.");
×
198
    }
199
    String candidate = input.trim();
3✔
200

201
    if (candidate.startsWith("http://")) {
4✔
202
      throw new IllegalArgumentException("Only HTTPS URLs are supported: " + input);
6✔
203
    }
204

205
    if (candidate.startsWith("https://")) {
4✔
206
      return parseEndpointFromUri(input, URI.create(candidate));
5✔
207
    }
208

209
    if (candidate.contains("://")) {
4!
210
      URI uri = URI.create(candidate);
×
211
      String scheme = uri.getScheme();
×
212
      if ((scheme == null) || !"https".equals(scheme.toLowerCase(Locale.ROOT))) {
×
213
        throw new IllegalArgumentException("Only HTTPS URLs are supported: " + input);
×
214
      }
215
      return parseEndpointFromUri(input, uri);
×
216
    }
217

218
    int separatorIndex = candidate.lastIndexOf(':');
4✔
219
    if (separatorIndex > 0 && separatorIndex < (candidate.length() - 1) && candidate.indexOf(':') == separatorIndex) {
13!
220
      String host = candidate.substring(0, separatorIndex).trim();
6✔
221
      String portPart = candidate.substring(separatorIndex + 1).trim();
7✔
222
      int port;
223
      try {
224
        port = Integer.parseInt(portPart);
3✔
225
      } catch (NumberFormatException e) {
×
226
        throw new IllegalArgumentException("Invalid port in input: " + input, e);
×
227
      }
1✔
228
      validateEndpoint(host, port, input);
4✔
229
      return new TlsEndpoint(host, port);
6✔
230
    }
231

232
    validateEndpoint(candidate, 443, input);
×
233
    return new TlsEndpoint(candidate, 443);
×
234
  }
235

236
  private static TlsEndpoint parseEndpointFromUri(String input, URI uri) {
237
    String host = uri.getHost();
3✔
238
    int port = (uri.getPort() > 0) ? uri.getPort() : 443;
8✔
239
    validateEndpoint(host, port, input);
4✔
240
    return new TlsEndpoint(host, port);
6✔
241
  }
242

243
  private static void validateEndpoint(String host, int port, String input) {
244
    if ((host == null) || host.isBlank()) {
5!
245
      throw new IllegalArgumentException("Missing host in input: " + input);
×
246
    }
247
    if ((port < 1) || (port > 65535)) {
6!
248
      throw new IllegalArgumentException("Port out of range in input: " + input);
×
249
    }
250
  }
1✔
251

252
  /**
253
   * Checks if a TLS endpoint can be reached and validated with the current default trust configuration.
254
   *
255
   * @param host the server host to connect to.
256
   * @param port the server port to connect to.
257
   * @return {@code true} if TLS handshake succeeds without truststore changes, {@code false} otherwise.
258
   */
259
  public static boolean isReachable(String host, int port) {
260
    validateEndpoint(host, port, host + ":" + port);
6✔
261
    try {
262
      SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL);
3✔
263
      sslContext.init(null, null, new SecureRandom());
7✔
264
      SSLSocketFactory factory = sslContext.getSocketFactory();
3✔
265

266
      try (SSLSocket socket = connectTlsSocket(factory, host, port)) {
×
267
        socket.startHandshake();
×
268
      }
269
      return true;
×
270
    } catch (Exception e) {
1✔
271
      return false;
2✔
272
    }
273

274
  }
275

276
  /**
277
   * Checks if a TLS endpoint can be reached and validated using the provided custom truststore.
278
   *
279
   * @param host the server host to connect to.
280
   * @param port the server port to connect to.
281
   * @param truststorePath the path to the custom truststore to use.
282
   * @return {@code true} if TLS handshake succeeds with the custom truststore, {@code false} otherwise.
283
   */
284
  public static boolean isReachable(String host, int port, Path truststorePath) {
285
    validateEndpoint(host, port, host + ":" + port);
×
286
    Objects.requireNonNull(truststorePath, "truststorePath");
×
287
    try {
288
      verifyConnectionWithTruststore(host, port, truststorePath);
×
289
      return true;
×
290
    } catch (Exception e) {
×
291
      return false;
×
292
    }
293
  }
294

295
  private static SSLSocket connectTlsSocket(SSLSocketFactory factory, String host, int port) throws Exception {
296
    SSLSocket socket = (SSLSocket) factory.createSocket();
4✔
297
    try {
298
      socket.connect(new InetSocketAddress(host, port), DEFAULT_TIMEOUT_MILLIS);
×
299
      socket.setSoTimeout(DEFAULT_TIMEOUT_MILLIS);
×
300
      return socket;
×
301
    } catch (Exception e) {
1✔
302
      try {
303
        socket.close();
2✔
304
      } catch (Exception ignored) {
×
305
        // ignore close failures on unsuccessful connect
306
      }
1✔
307
      throw e;
2✔
308
    }
309
  }
310

311
  /**
312
   * Fetches the server certificate from the specified host and port by performing a TLS handshake and capturing the certificate chain using a custom trust
313
   * manager.
314
   *
315
   * @param host the server host to connect to.
316
   * @param port the server port to connect to.
317
   * @return the server certificate captured from the TLS handshake.
318
   * @throws Exception if an error occurs while fetching the server certificate, e.g. due to connection issues or if the server does not provide a
319
   *     certificate chain.
320
   */
321
  public static X509Certificate fetchServerCertificate(String host, int port) throws Exception {
322
    Objects.requireNonNull(host, "host");
4✔
323
    if (host.isBlank()) {
3✔
324
      throw new IllegalArgumentException("host must not be blank");
5✔
325
    }
326
    if ((port < 1) || (port > 65535)) {
6!
327
      throw new IllegalArgumentException("port must be between 1 and 65535");
5✔
328
    }
329

330
    SavingTrustManager savingTrustManager = new SavingTrustManager();
4✔
331

332
    SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL);
3✔
333
    sslContext.init(null, new TrustManager[] { savingTrustManager }, new SecureRandom());
12✔
334

335
    SSLSocketFactory factory = sslContext.getSocketFactory();
3✔
336
    try (SSLSocket socket = connectTlsSocket(factory, host, port)) {
×
337
      socket.startHandshake();
×
338
    } catch (SSLException e) {
×
339
      // expected: trust manager aborts after capturing the chain
340
    }
×
341

342
    X509Certificate[] chain = savingTrustManager.getChain();
×
343
    if ((chain == null) || (chain.length == 0)) {
×
344
      throw new CertificateException("Could not capture server certificate chain from " + host + ":" + port);
×
345
    }
346

347
    return chain[chain.length - 1];
×
348
  }
349

350
  /**
351
   * Verifies that a TLS connection to the specified host and port can be established using the truststore at the given path by performing a TLS handshake. If
352
   * the handshake is successful, the method returns normally. If the handshake fails due to trust issues, an SSLException is thrown.
353
   *
354
   * @param host the server host to connect to.
355
   * @param port the server port to connect to.
356
   * @param truststorePath the path to the truststore file to use for the TLS handshake.
357
   * @throws Exception if an error occurs while verifying the connection, e.g. due to connection issues, TLS handshake failure, or if the truststore file
358
   *     cannot be loaded.
359
   */
360
  public static void verifyConnectionWithTruststore(String host, int port, Path truststorePath) throws Exception {
361
    KeyStore truststore = KeyStore.getInstance("PKCS12");
×
362
    try (InputStream in = Files.newInputStream(truststorePath)) {
×
363
      truststore.load(in, CUSTOM_TRUSTSTORE_PASSWORD.toCharArray());
×
364
    }
365

366
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
×
367
    tmf.init(truststore);
×
368

369
    SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL);
×
370
    sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
×
371

372
    SSLSocketFactory socketFactory = sslContext.getSocketFactory();
×
373
    try (SSLSocket socket = (SSLSocket) socketFactory.createSocket(host, port)) {
×
374
      socket.setSoTimeout(DEFAULT_TIMEOUT_MILLIS);
×
375
      socket.startHandshake();
×
376
    }
377
  }
×
378

379
  /**
380
   * Generates a human-readable description of the given X.509 certificate including subject, issuer, serial number, validity period, signature algorithm, and
381
   *
382
   * @param certificate the certificate to describe.
383
   * @return a human-readable description of the given X.509 certificate.
384
   */
385
  public static String describeCertificate(X509Certificate certificate) {
386
    String nl = "\n";
2✔
387
    StringBuilder sb = new StringBuilder();
4✔
388
    sb.append("Subject: ").append(certificate.getSubjectX500Principal()).append(nl);
9✔
389
    sb.append("Issuer : ").append(certificate.getIssuerX500Principal()).append(nl);
9✔
390
    sb.append("Serial : ").append(certificate.getSerialNumber()).append(nl);
9✔
391
    sb.append("Valid  : ").append(certificate.getNotBefore()).append(" -> ").append(certificate.getNotAfter()).append(nl);
14✔
392
    sb.append("SigAlg : ").append(certificate.getSigAlgName()).append(nl);
9✔
393

394
    Set<String> critical = certificate.getCriticalExtensionOIDs();
3✔
395
    Set<String> nonCritical = certificate.getNonCriticalExtensionOIDs();
3✔
396
    sb.append("Critical extensions    : ").append((critical == null) ? "[]" : critical).append(nl);
10!
397
    sb.append("Non-critical extensions: ").append((nonCritical == null) ? "[]" : nonCritical);
8!
398

399
    return sb.toString();
3✔
400
  }
401

402
  private static boolean containsCertificate(KeyStore keyStore, X509Certificate certificate) throws Exception {
403
    Enumeration<String> aliases = keyStore.aliases();
3✔
404
    while (aliases.hasMoreElements()) {
3✔
405
      String alias = aliases.nextElement();
4✔
406
      Certificate existing = keyStore.getCertificate(alias);
4✔
407
      if ((existing instanceof X509Certificate existingX509) && Arrays.equals(existingX509.getEncoded(), certificate.getEncoded())) {
12!
408
        return true;
2✔
409
      }
410
    }
1✔
411
    return false;
2✔
412
  }
413

414
  private static String makeUniqueAlias(KeyStore keyStore, String baseAlias) throws Exception {
415
    String alias = baseAlias;
2✔
416
    int i = 1;
2✔
417
    while (keyStore.containsAlias(alias)) {
4!
418
      alias = baseAlias + "-" + i;
×
419
      i++;
×
420
    }
421
    return alias;
2✔
422
  }
423

424
  private static final class SavingTrustManager implements X509TrustManager {
425

426
    private X509Certificate[] chain;
427

428
    @Override
429
    public void checkClientTrusted(X509Certificate[] chain, String authType) {
430
      // not needed
431
    }
×
432

433
    @Override
434
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
435
      this.chain = (chain == null) ? null : Arrays.copyOf(chain, chain.length);
×
436
      if ((chain == null) || (chain.length == 0)) {
×
437
        throw new CertificateException("Server certificate chain is empty");
×
438
      }
439
      throw new CertificateException("Captured server certificate chain");
×
440
    }
441

442
    @Override
443
    public X509Certificate[] getAcceptedIssuers() {
444
      return new X509Certificate[0];
×
445
    }
446

447
    public X509Certificate[] getChain() {
448
      return this.chain;
×
449
    }
450
  }
451
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc