API Java KeyStore

Haut Java

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Vue d'ensemble

Dans ce didacticiel, nous examinons la gestion des clés cryptographiques et des certificats en Java à l'aide de l' API KeyStore .

2. Keystores

Si nous devons gérer les clés et les certificats en Java, nous avons besoin d'un magasin de clés , qui est simplement une collection sécurisée d' entrées alias de clés et de certificats.

Nous enregistrons généralement les fichiers de clés dans un système de fichiers et nous pouvons le protéger avec un mot de passe.

Par défaut, Java a un fichier keystore situé dans JAVA_HOME / jre / lib / security / cacerts . Nous pouvons accéder à ce fichier de clés en utilisant le changement de mot de passe du fichier de clés par défaut .

Maintenant, avec ce peu de contexte, passons à la création de notre premier.

3. Création d'un keystore

3.1. Construction

Nous pouvons facilement créer un keystore à l'aide de keytool, ou nous pouvons le faire par programme à l'aide de l' API KeyStore :

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

Ici, nous utilisons le type par défaut, bien qu'il existe quelques types de keystore disponibles comme jceks ou pcks12 .

Nous pouvons remplacer le type par défaut «JKS» (un protocole de stockage de clés propriétaire d'Oracle) en utilisant un paramètre -Dkeystore.type :

-Dkeystore.type=pkcs12

Ou, nous pouvons, bien sûr, lister l'un des formats pris en charge dans getInstance :

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

3.2. Initialisation

Au départ, nous devons charger le keystore:

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

Nous utilisons load que nous créons un nouveau keystore ou que nous en ouvrions un existant.

Et nous demandons à KeyStore d'en créer un nouveau en passant null comme premier paramètre.

Nous fournissons également un mot de passe, qui sera utilisé pour accéder au keystore à l'avenir. Nous pouvons également définir ceci sur null , bien que cela rendrait nos secrets ouverts.

3.3. Espace de rangement

Enfin, nous sauvegardons notre nouveau keystore dans le système de fichiers:

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

Notez que les exceptions vérifiées ne sont pas affichées ci-dessus pour obtenirInstance , charger et stocker chaque lancement.

4. Chargement d'un keystore

Pour charger un keystore, nous devons d'abord créer une instance KeyStore , comme auparavant.

Cette fois, cependant, spécifions le format puisque nous en chargeons un existant:

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

Si notre JVM ne prend pas en charge le type de keystore que nous avons passé, ou s'il ne correspond pas au type du keystore sur le système de fichiers que nous ouvrons, nous obtiendrons une KeyStoreException :

java.security.KeyStoreException: KEYSTORE_TYPE not found

De plus, si le mot de passe est incorrect, nous obtiendrons une exception UnrecoverableKeyException:

java.security.UnrecoverableKeyException: Password verification failed

5. Stockage des entrées

Dans le keystore, nous pouvons stocker trois types d'entrées différents, chaque entrée sous son alias:

  • Clés symétriques (appelées clés secrètes dans JCE),
  • Clés asymétriques (appelées clés publiques et privées dans JCE), et
  • Certificats de confiance

Jetons un coup d'œil à chacun d'eux.

5.1. Enregistrer une clé symétrique

La chose la plus simple que nous pouvons stocker dans un keystore est une clé symétrique.

Pour enregistrer une clé symétrique, nous avons besoin de trois choses:

  1. un alias - c'est simplement le nom que nous utiliserons à l'avenir pour faire référence à l'entrée
  2. une clé - qui est enveloppée dans un KeyStore.SecretKeyEntry .
  3. un mot de passe - qui est enveloppé dans ce qu'on appelle un 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 nous voulons supprimer notre keystore, l'API ne nous aide pas, mais nous pouvons toujours utiliser Java pour le faire:

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

Ou, comme alternative, nous pouvons conserver le keystore et simplement supprimer les entrées:

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

9. Conclusion

Dans cet article, nous avons parlé de la gestion des certificats et des clés à l'aide de l' API KeyStore . Nous avons discuté de ce qu'est un keystore, comment en créer, charger et supprimer un, comment stocker une clé ou un certificat dans le keystore et comment charger et mettre à jour des entrées existantes avec de nouvelles valeurs.

La mise en œuvre complète de l'exemple peut être trouvée sur Github.

Fond Java

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS