9

Extract Certificate and add it to a Truststore Programmatically

 11 months ago
source link: https://tech.asimio.net/2023/09/10/Extract-Certificate-Create-Truststore-Programmatically.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Extract Certificate and add it to a Truststore Programmatically

Orlando L Otero | Sep 10, 2023

| java, jdk, jre, ssl, truststore

| 4 min read

| 0 Comments

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.

Warning: A word of caution, be aware of adding a self-signed certificate to a production TrustStore. It’s not recommended and you should have very good reasons to do so.
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—–
Warning: A word of caution, don’t skip validating certificate chains in production environments unless you have very good reasons to do so.
  • 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.

5. SOURCE CODE

Accompanying source code for this blog post can be found at:


NEED HELP?

I provide Consulting Services.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK