Search code examples
javaspring-bootssl-certificatetruststore

Setting trustStoreType is not working properly in Spring Boot


We have a Spring Boot application where we need to set javax.net.ssl.trustStoreType to WINDOWS-ROOT.

System.setProperty("javax.net.ssl.trustStoreType", "WINDOWS-ROOT");

I have a class named TestService in my src folder (not from test package) to see if the target URL is accessible properly, without SSL error. I'm adding this class and running application to manually check the result. It's working fine.

@Service
public class TestService {
    private RestTemplate restTemplate;

    public TestService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    @EventListener(ApplicationReadyEvent.class)
    public void testSslAccesibility() {
       try {
           ResponseEntity<String> response = restTemplate.getForEntity("https://THE_URL_WE_RE_TESTING", String.class);
       }
       catch (Exception e) {
           if(e.getCause() instanceof SSLHandshakeException) {
               logger.debug("SSL CERTIFICATE ERROR");
           }
       }
   }
}

The problem is, even though we set the trustStoreType property during initialization(before test method run), the behavior changes according to which bean I placed it in.

If I set it in constructor of the bean where we get some part of application.properties, the test method works and gets actual response from target server. (Below I hide the actual names by renaming classes and variables)

@ConstructorBinding
@ConfigurationProperties("myconf")
public class MyConfProperties {
    public final ConfSet1 confSet1; 
    public final ConfSet2 confSet2;

    public MyConfProperties(ConfSet1 confSet1, ConfSet2 confSet2) {
        this.confSet1 = confSet1;
        this.confSet2 = confSet2;

        if(confSet2.useWindowsStore) {
            System.setProperty("javax.net.ssl.trustStoreType", "WINDOWS-ROOT");
        }
    }
    
    public static class ConfSet1 {
        public final String cs1attr1;
        public final String cs1attr2;
        
        public ConfSet1(String cs1attr1, String cs1attr2) {
            this.cs1attr1 = cs1attr1;
            this.cs1attr2 = cs1attr2;
        }
    }

    public static class ConfSet2 {
        public final String cs2attr1;
        public final String useWindowsStore;
        
        public ConfSet2(String cs2attr1, String useWindowsStore) {
            this.cs2attr1 = cs2attr1;
            this.useWindowsStore = useWindowsStore;
        }
    }
}

If I set it in a new separate bean, it's set but takes no effect. The test fails with SSL CERTIFICATE error.

@Component
public class StoreTypeManager {
    private MyConfProperties.confSet2 properties;

    public StoreTypeManager(MyConfProperties myConfProperties) {
        this.properties = myConfProperties.confSet2;

        if(properties.useWindowsStore) {
            System.setProperty("javax.net.ssl.trustStoreType", "WINDOWS-ROOT");
        }
    }
}

I only change the place of a single System.setProperty() line.

For the timing when I debug, new bean is created much more later. But normally, it should not matter.

  • What is the reason of this strange behavior?
  • How can I make it work even if executed later in the initialization?
  • If I cannot, how can I adjust the new bean to initialize earlier?

Solution

  • I solve the problem by initializing my custom SSLContext and setting as default, after setting the trustStoreType system property.

    This solution works, so it indicates that when I set the system property during late stage, the default SSLContext was already created at somewhere and will not be affected by setting system property after creation. So it's risky to set javax.net.ssl.trustStoreType or other ssl related properties at runtime, unless you're using below solution. Setting them as JVM options would be safe.

    if(properties.useWindowsStore) {
        System.setProperty("javax.net.ssl.trustStoreType", "WINDOWS-ROOT");
    }
    
    /*
    * Initializing a TrustManagerFactory with null is configuring TrustManager to use system's default keystore.
    * If useWindowsStore = true then system certificates are from OS, else system certificates are from JDK.
    */
    TrustManagerFactory trustManagerFactorySystem = TrustManagerFactory.getInstance("X509");
    trustManagerFactorySystem.init((KeyStore) null);
    
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagerFactorySystem.getTrustManagers(), null);
    SSLContext.setDefault(sslContext);
    

    If you want to merge system certificates with some of your certificates, you could initiate another TrustManagerFactory with a new KeyStore and combine them into one TrustManager as described in this question: Registering multiple keystores in JVM