API de Java KeyStore

Parte superior de Java

Acabo de anunciar el nuevo curso Learn Spring , centrado en los fundamentos de Spring 5 y Spring Boot 2:

>> VER EL CURSO

1. Información general

En este tutorial, estamos analizando la gestión de certificados y claves criptográficas en Java mediante la API KeyStore .

2. Almacenes de claves

Si necesitamos administrar claves y certificados en Java, necesitamos un almacén de claves , que es simplemente una colección segura de entradas de claves y certificados con alias .

Por lo general, guardamos los almacenes de claves en un sistema de archivos y podemos protegerlos con una contraseña.

De forma predeterminada, Java tiene un archivo de almacén de claves ubicado en JAVA_HOME / jre / lib / security / cacerts . Podemos acceder a este almacén de claves utilizando la contraseña predeterminada del almacén de claves changeit .

Ahora, con ese poco de trasfondo, vamos a crear el primero.

3. Creación de un almacén de claves

3.1. Construcción

Podemos crear fácilmente un almacén de claves usando keytool, o podemos hacerlo mediante programación usando la API de KeyStore :

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

Aquí, usamos el tipo predeterminado, aunque hay algunos tipos de almacén de claves disponibles como jceks o pcks12 .

Podemos anular el tipo predeterminado "JKS" (un protocolo de almacén de claves propiedad de Oracle) mediante un parámetro -Dkeystore.type :

-Dkeystore.type=pkcs12

O, por supuesto, podemos enumerar uno de los formatos admitidos en getInstance :

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

3.2. Inicialización

Inicialmente, necesitamos cargar el almacén de claves:

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

Usamos load ya sea que estemos creando un nuevo almacén de claves o abriendo uno existente.

Y le decimos a KeyStore que cree uno nuevo pasando nulo como primer parámetro.

También proporcionamos una contraseña, que se utilizará para acceder al almacén de claves en el futuro. También podemos establecer esto en nulo , aunque eso haría que nuestros secretos se abran.

3.3. Almacenamiento

Finalmente, guardamos nuestro nuevo almacén de claves en el sistema de archivos:

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

Tenga en cuenta que no se muestran arriba son las varias excepciones marcadas que obtienen , cargan y almacenan cada lanzamiento.

4. Carga de un almacén de claves

Para cargar un almacén de claves, en primer lugar necesidad de crear un almacén de claves ejemplo, como antes.

Esta vez, sin embargo, especifiquemos el formato ya que estamos cargando uno existente:

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

Si nuestra JVM no admite el tipo de almacén de claves que pasamos, o si no coincide con el tipo de almacén de claves en el sistema de archivos que estamos abriendo, obtendremos una KeyStoreException :

java.security.KeyStoreException: KEYSTORE_TYPE not found

Además, si la contraseña es incorrecta, obtendremos una UnrecoverableKeyException:

java.security.UnrecoverableKeyException: Password verification failed

5. Almacenamiento de entradas

En el almacén de claves, podemos almacenar tres tipos diferentes de entradas, cada entrada bajo su alias:

  • Claves simétricas (denominadas claves secretas en el JCE),
  • Claves asimétricas (denominadas claves públicas y privadas en el JCE) y
  • Certificados de confianza

Echemos un vistazo a cada uno.

5.1. Guardar una clave simétrica

Lo más simple que podemos almacenar en un almacén de claves es una clave simétrica.

Para guardar una clave simétrica, necesitaremos tres cosas:

  1. un alias : este es simplemente el nombre que usaremos en el futuro para hacer referencia a la entrada
  2. una clave , que está envuelta en un KeyStore.SecretKeyEntry .
  3. una contraseña , que está envuelta en lo que se denomina 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 certificateitself, 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

Si queremos eliminar nuestro almacén de claves, la API no nos ayuda, pero aún podemos usar Java para hacerlo:

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

O, como alternativa, podemos mantener el almacén de claves y simplemente eliminar las entradas:

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

9. Conclusión

En este artículo, hablamos sobre la gestión de certificados y claves mediante la API de KeyStore . Discutimos qué es un almacén de claves, cómo crear, cargar y eliminar uno, cómo almacenar una clave o certificado en el almacén de claves y cómo cargar y actualizar las entradas existentes con nuevos valores.

La implementación completa del ejemplo se puede encontrar en Github.

Fondo de Java

Acabo de anunciar el nuevo curso Learn Spring , centrado en los fundamentos de Spring 5 y Spring Boot 2:

>> VER EL CURSO