<

I just announced the newSpring Security 5 modules (primarily focused on OAuth2) in the course:

>> CHECK OUT LEARN SPRING SECURITY

1. Overview

In this tutorial, we’re looking at managing cryptographic keys and certificates in Java using the KeyStore API.

2. Keystores

If we need to manage keys and certificates in Java, we need a keystore, which is simply a secure collection of aliased entries of keys and certificates.

We typically save keystores to a file system, and we can protect it with a password.

By default, Java has a keystore file located at JAVA_HOME/jre/lib/security/cacerts. We can access this keystore using the default keystore password changeit.

Now, with that bit of background, let’s get to creating our first one.

3. Creating a Keystore

3.1. Construction

We can easily create a keystore using keytool, or we can do it programmatically using the KeyStore API:

KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());

Here, we use the default type, though there are a few keystore types available like jceks or pcks12.

We can override the default “JKS”(an Oracle-proprietary keystore protocol) type using a -Dkeystore.type parameter:

-Dkeystore.type=pkcs12

Or, we can, of course, list one of the supported formats in getInstance:

KeyStore ks = KeyStore.getInstance("pcks12");

3.2. Initialization

Initially, we need to load the keystore:

char[] pwdArray = "password".toCharArray();
ks.load(null, pwdArray);

We use load whether we’re creating a new keystore or opening up an existing one.

And, we tell KeyStore to create a new one by passing null as the first parameter.

We also provide a password, which will be used for accessing the keystore in the future. We can also set this to null, though that would make our secrets open.

3.3. Storage

Finally, we save our new keystore to the file system:

try (FileOutputStream fos = new FileOutputStream("newKeyStoreFileName.jks")) {
    ks.store(fos, pwdArray);
}

Note that not shown above are the several checked exceptions that getInstanceload, and store each throw.

4. Loading a Keystore

To load a keystore, we first need to create a KeyStore instance, like before.

This time, though, let’s specify the format since we are loading an existing one:

KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("newKeyStoreFileName.jks"), pwdArray);

If our JVM doesn’t support the keystore type we passed, or if it doesn’t match the type of the keystore on the filesystem that we’re opening, we’ll get a KeyStoreException:

java.security.KeyStoreException: KEYSTORE_TYPE not found

Also, if the password is wrong, we’ll get an UnrecoverableKeyException:

java.security.UnrecoverableKeyException: Password verification failed

5. Storing Entries

In the keystore, we can store three different kinds of entries, each entry under its alias:

  • Symmetric Keys (referred to as Secret Keys in the JCE),
  • Asymmetric Keys (referred to as Public and Private Keys in the JCE), and
  • Trusted Certificates

Let’s take a look at each one.

5.1. Saving a Symmetric Key

The simplest thing we can store in a keystore is a Symmetric Key.

To save a symmetric key, we’ll need three things:

  1. an alias – his is simply the name that we’ll use in the future to refer to the entry
  2. a key – which is wrapped in a KeyStore.SecretKeyEntry.
  3. a password – which is wrapped in what is called a ProtectionParam.
KeyStore.SecretKeyEntry secret
 = new KeyStore.SecretKeyEntry(secretKey);
KeyStore.ProtectionParameter password
 = new KeyStore.PasswordProtection(pwdArray);
ks.setEntry("db-encryption-secret", secret, password);

Keep in mind that the password cannot be null, however, it can be an empty String. If we leave the password null for an entry, we’ll get a KeyStoreException:

java.security.KeyStoreException: non-null password required to create SecretKeyEntry

It may seem a little weird that we need to wrap the key and the password in wrapper classes.

We wrap the key because setEntry is a generic method that can be used for the other entry types as well. The type of entry allows the KeyStore API to treat it differently.

We wrap the password because the KeyStore API supports callbacks to GUIs and CLIs to collect the password from the end user. Check out the KeyStore.CallbackHandlerProtection Javadoc for more details.

We can also use this method to update an existing key. We just need to call it again with the same alias and password and our new secret.

5.2. Saving a Private Key

Storing asymmetric keys is a bit more complex since we need to deal with certificate chains.

Also, the KeyStore API gives us a dedicated method called setKeyEntry which is more convenient than the generic setEntry method.

So, to save an asymmetric key, we’ll need four things:

  1. an alias, same as before
  2. a private key. Because we aren’t using the generic method, the key won’t get wrapped. Also, for our case, it should be an instance of PrivateKey
  3. a password for accessing the entry. This time, the password is mandatory
  4. a certificate chain that certifies the corresponding public key
X509Certificate[] certificateChain = new X509Certificate[2];
chain[0] = clientCert;
chain[1] = caCert;
ks.setKeyEntry("sso-signing-key", privateKey, pwdArray, certificateChain);

Now, lots can go wrong here, of course, like if pwdArray is null:

java.security.KeyStoreException: password can't be null

But, there’s a really strange exception to be aware of, and that is if pwdArray is an empty array:

java.security.UnrecoverableKeyException: Given final block not properly padded

To update, we can simply call the method again with the same alias and a new privateKey and certificateChain.

Also, it might be valuable to do a quick refresher on how to generate a certificate chain.

5.3. Saving a Trusted Certificate

Storing trusted certificates is quite simple. It only requires the alias and the certificate itself, which is of type Certificate:

ks.setCertificateEntry("google.com", trustedCertificate);

Usually, the certificate is one that we didn’t generate, but that came from a third-party.

Because of that, it’s important to note here that KeyStore doesn’t actually verify this certificate. We should verify it on our own before storing it.

To update, we can simply call the method again with the same alias and a new trustedCertificate.

6. Reading Entries

Now that we’ve written some entries, we’ll certainly want to read them.

6.1. Reading a Single Entry

First, we can pull keys and certificates out by their alias:

Key ssoSigningKey = ks.getKey("sso-signing-key", pwdArray);
Certificate google = ks.getCertificate("google.com");

If there’s no entry by that name or it is of a different type, then getKey simply returns null:

public void whenEntryIsMissingOrOfIncorrectType_thenReturnsNull() {
    // ... initialize keystore
    // ... add an entry called "widget-api-secret"

   Assert.assertNull(ks.getKey("some-other-api-secret"));
   Assert.assertNotNull(ks.getKey("widget-api-secret"));
   Assert.assertNull(ks.getCertificate("widget-api-secret")); 
}

But, if the password for the key is wrong, we’ll get that same odd error we talked about earlier:

java.security.UnrecoverableKeyException: Given final block not properly padded

6.2. Checking if a Keystore Contains an Alias

Since KeyStore just stores entries using a Map, it exposes the ability to check for existence without retrieving the entry:

public void whenAddingAlias_thenCanQueryWithoutSaving() {
    // ... initialize keystore
    // ... add an entry called "widget-api-secret"
    assertTrue(ks.containsAlias("widget-api-secret"));
    assertFalse(ks.containsAlias("some-other-api-secret"));
}

6.3. Checking the Kind of Entry

Or, KeyStore#entryInstanceOf is a bit more powerful.

It’s like containsAlias, except it also checks the entry type:

public void whenAddingAlias_thenCanQueryByType() {
    // ... initialize keystore
    // ... add a secret entry called "widget-api-secret"
    assertTrue(ks.containsAlias("widget-api-secret"));
    assertFalse(ks.entryInstanceOf(
      "widget-api-secret",
      KeyType.PrivateKeyEntry.class));
}

7. Deleting Entries

KeyStore, of course, supports deleting the entries we’ve added:

public void whenDeletingAnAlias_thenIdempotent() {
    // ... initialize a keystore
    // ... add an entry called "widget-api-secret"
    assertEquals(ks.size(), 1);
    ks.deleteEntry("widget-api-secret");
    ks.deleteEntry("some-other-api-secret");
    assertFalse(ks.size(), 0);
}

Fortunately, deleteEntry is idempotent, so the method reacts the same, whether the entry exists or not.

8. Deleting a Keystore

If we want to delete our keystore, the API is no help to us, but we can still use Java to do it:

Files.delete(Paths.get(keystorePath));

Or, as an alternative, we can keep the keystore around, and just remove entries:

Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
    String alias = aliases.nextElement();
    keyStore.deleteEntry(alias);
}

9. Conclusion

In this article, we talked about managing certificates and keys using KeyStore API. We discussed what a keystore is, how to create, load and delete one, how to store a key or certificate in the keystore and how to load and update existing entries with new values.

The full implementation of the example can be found over on Github.

I just announced the new Spring Security 5 modules (primarily focused on OAuth2) in the course:

>> CHECK OUT LEARN SPRING SECURITY

Leave a Reply

Be the First to Comment!

avatar
  Subscribe  
Notify of