Certificates and the JVM

The recent events revealed that the internet is not a very safe place. If you don’t encrypt your data, it is likely to be analyzed by third parties like intelligence services. So you start to encrypt everything because you care for the safety your customers data. You managed to get it done without extra budget because you use a self-signed certificate – but oh, what is this? A nasty ValidatorException saying “PKIX path building failed”. What now?

I assume you understand how encryption using private and public keys works. In short, there are some root certificates which are trusted globally. Every browser uses a set of those certificates, often referred to as “certificate authority (CA) certs”. The JVM also has such a set – but unfortunately it doesn’t contain the certificate you created!

One possibility to solve this would be to add your certificate to the JVM’s trust store on disk. But then you would have to repeat this step after every update, which seems unpractical. The other possibility seems much more appealing: let your application add any missing certificate to the trust store on the fly, in memory, when it starts up. You don’t have to repeat adding the cert manually. You don’t even change the trust store file on disk!

Here’s how it can work: you put the cert(s) you want to have available somewhere inside you project, let’s say they are located at /certs/MyOwnCertificate.pem and /certs/MyOtherOwnCertificate.pem now (speaking of the class path). Then you make your app call loadCerts(“certs/*.pem”) from the code snippet below (full code with imports here) – done!

	public static void loadCerts(String classpathWildcardSearch) {
		File keyStoreLocation = getKeyStoreLocation();
		keyStore = loadKeyStore(keyStoreLocation);
		
		Resource[] resources = findResources(classpathWildcardSearch);
		if (resources != null) {
			for (Resource resource : resources) {
				handleCertificate(resource);
			}
			updateDefaultTrustManager();
		}
	}
	
	private static File getKeyStoreLocation() {
		File directory = new File(System.getProperty("java.home") + File.separatorChar
			+ "lib" + File.separatorChar + "security");
		File keyStoreFile = new File(directory, "jssecacerts");
		if (!keyStoreFile.exists() || !keyStoreFile.isFile()) {
			keyStoreFile = new File(directory, "cacerts");
		}
		return keyStoreFile;
	}
	
	private static KeyStore loadKeyStore(File keyStoreLocation) {
		LOG.debug("load key store {}", keyStoreLocation.getAbsolutePath());
		try (InputStream keyStoreInputStream = new FileInputStream(keyStoreLocation)) {
			KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
			defaultKeyStore.load(keyStoreInputStream, KEY_STORE_PASSWORD.toCharArray());
			return defaultKeyStore;
		} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
			String message = "key store " + keyStoreLocation.getAbsolutePath() + " could not be loaded";
			LOG.warn(message, e);
			throw new RuntimeException(message, e);
		}
	}
	
	private static Resource[] findResources(String classpathWildcardSearch) {
		PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
		Resource[] resources = null;
		try {
			resources = resolver.getResources("classpath:" + classpathWildcardSearch);
		} catch (IOException e) {
			LOG.warn("certificates could not be found");
		}
		return resources;
	}
	
	private static void handleCertificate(Resource resource) {
		String certificateName =
			resource.getDescription().replaceAll("^.*\\" + File.separator, "").replaceAll("]$", "");
		try {
			URL source = resource.getURL();
			Certificate certificate =
				CertificateFactory.getInstance(CERTIFICATE_FACTORY_TYPE).generateCertificate(source.openStream());
			String certificateAlias = keyStore.getCertificateAlias(certificate);
			if (certificateAlias == null) {
				LOG.info("certificate {} not found, adding it", certificateName);
				keyStore.setCertificateEntry(certificateName, certificate);
			}
		} catch (IOException | CertificateException | KeyStoreException e) {
			LOG.warn("certificate {} could not be processed", resource.getDescription());
		}
	}
	
	private static void updateDefaultTrustManager() {
		try {
			TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TRUST_MANAGER_FACTORY_TYPE);
			trustManagerFactory.init(keyStore);
			TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
			SSLContext sc = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL);
			sc.init(null, trustManagers, null);
			SSLContext.setDefault(sc);
		} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
			LOG.warn("key store could not be set as default");
		}
	}