1. OVERVIEW

A few days ago I was working on the accompanying source code for:

blog posts, and ran into the same issue a few times.

The previously running azure-cosmos-emulator Docker container wouldn’t start.

I had to run a new azure-cosmos-emulator container, but every time I ran a new container, the emulator’s PEM-encoded certificate changed. That meant the Spring Boot application failed to start because it couldn’t connect to the Cosmos DB anymore. The SSL handshake failed.

A manual solution involved running a bash script that deletes the invalid certificate from the TrustStore, and adds a new one generated during the new Docker container start-up process.

That would be fine if you only need to use this approach once or two during the development phase.

But this manual approach won’t work for running Integration Tests as part of your CI/CD pipeline, regardless if an azure-cosmos-emulator container is already running, or if you rely on Testcontainers to run a new one.

This blog post covers how to programmatically extract an SSL certificate from a secure connection and add it to a TrustStore that you can use in your integration tests, for instance.

New Cosmos DB emulator containers generate different PEM files New Cosmos DB emulator containers generate different PEM files

2. UTILITY CLASS

Let’s start with a utility class that you could use in your Java applications.

KeyStoreUtils.java:

public final class KeyStoreUtils {

  public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
  public static final String END_CERT = "-----END CERTIFICATE-----";
// ...
  public static void extractPublicCert(String host, int port, String savePemToFilePath)
      throws GeneralSecurityException, IOException {

    // Create a trust manager that does not validate certificate chains
    TrustManager[] allTrustManagers = buildAllTrustManagers(savePemToFilePath);

    // Install the all trust managers
    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, allTrustManagers, new SecureRandom());

    // Open socket, handshake, get certs
    SSLSocket socket = (SSLSocket) sc.getSocketFactory().createSocket(host, port);
    socket.startHandshake();
    Certificate[] certs = socket.getSession().getPeerCertificates();

    writePemCertificate(savePemToFilePath, certs[0].getEncoded());
  }

  public static void createKeyStore(List<String> certFilesPaths, String keyStorePath, String keyStorePasswd)
      throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {

    KeyStore keyStore = KeyStore.getInstance("JKS");
    keyStore.load(null, null);

    for (String certFile : certFilesPaths) {
      try (FileInputStream certificateStream = new FileInputStream(new File(certFile))) {
        while (certificateStream.available() > 0) {
          Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(certificateStream);
          keyStore.setCertificateEntry(certFile, certificate);
        }
      }
    }

    keyStore.store(new FileOutputStream(keyStorePath), keyStorePasswd.toCharArray());
  }

  private static void writePemCertificate(String savePemToFilePath, byte[] rawCertificate) throws IOException {
    // New line every 64 bytes
    Encoder base64Encoder = Base64.getMimeEncoder(64, System.lineSeparator().getBytes());

    try (FileOutputStream out = new FileOutputStream(savePemToFilePath)) {
      out.write(BEGIN_CERT.getBytes());
      out.write(System.lineSeparator().getBytes());
      out.write(base64Encoder.encode(rawCertificate));
      out.write(System.lineSeparator().getBytes());
      out.write(END_CERT.getBytes());
      out.write(System.lineSeparator().getBytes());
    }
  }

// ...
}
  • extractPublicCert() method:

    1. Builds a TrustManager array that doesn’t validate certificate chains.

    2. Connects to the (Host, IP) socket, starts a handshake, and retrieves the certificates.

    3. Writes the raw certificate in PEM format to the specified file path.

    —–BEGIN CERTIFICATE—–
    Base64 encoded 64 bytes each line
    —–END CERTIFICATE—–
  • createKeyStore() method creates a new KeyStore in the specified path, and adds the PEM-formatted certificates passed to the method.

3. USAGE IN INTEGRATION TESTS

This integration test class uses JUnit 5 (brought in when including spring-boot-starter-test:2.7.14.

UserControllerIntegrationTest.java:

// ... Uses JUnit 5
public class UserControllerIntegrationTest {
// ...

  @TempDir
  private static File TEMP_FOLDER;

  @BeforeAll
  public static void setupTruststore() throws Exception {
    String pemCertPath = TEMP_FOLDER.getPath() + File.separator + "cosmos-db-emulator.pem";
    KeyStoreUtils.extractPublicCert("localhost", 8081, pemCertPath);
    log.info("Public cert saved to {}", pemCertPath);

    String trustStorePath = TEMP_FOLDER.getPath() + File.separator + "integration-tests-cosmos-emulator.truststore";
    KeyStoreUtils.createKeyStore(
      Lists.newArrayList(pemCertPath),
      trustStorePath,
      "changeit"
    );
    log.info("Truststore set to {}", trustStorePath);

    System.setProperty("javax.net.ssl.trustStore", trustStorePath);
    System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
    System.setProperty("javax.net.ssl.trustStoreType", "PKCS12");
  }

  @Test
  public void shouldRetrieveUserWithIdEqualsTo100() {
    // ...
  }

// ...
}
  • @TempDir-annotated variable means this folder will only exist for the duration of the tests included in this class.

  • @BeforeAll-annotated setupTruststore() method runs only once during all tests execution.

    1. It uses KeyStoreUtils.java to extract a certificate from a secure socket and save it in PEM format, before adding it to a custom TrustStore for the integration tests to use.

    2. Both, the PEM certificate and TrustStore files are saved in the @TempDir-annotated folder. They are deleted when all tests run.

    3. It sets system properties javax.net.ssl.trustStore, javax.net.ssl.trustStorePassword, and javax.net.ssl.trustStoreType so that each test uses this TrustStore with the newly added certificate for successful connections between the Java application and the third-party system.

4. CONCLUSION

This blog post covered how to programmatically retrieve a certificate from an SSL socket, convert it to PEM format, and import it to a custom TrustStore for development or Integration Tests purposes.

You can automate your CI/CD pipeline by retrieving self-signed public certificates, and adding them to a custom TrustStore for successful connections between your Java applications and third-party systems like Cosmos databases.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.