1. 概要

In this tutorial, we’ll learn how to manage cryptographic keys and certificates in Java using the KeyStore API.

2. キーストア

Javaでキーと証明書を管理する必要がある場合は、キーストアが必要です。これは、キーと証明書のエイリアスエントリの安全なコレクションです。

通常、キーストアはファイルシステムに保存され、パスワードで保護できます。

デフォルトでは、Javaのキーストアファイルは JAVA_HOME / jre / lib / security /cacertsにあります。 このキーストアには、デフォルトのキーストアパスワードchangeitを使用してアクセスできます。

Now that we’ve established some background, let’s create our first one.

3. キーストアの作成

3.1. 工事

keytool を使用してキーストアを簡単に作成することも、 KeyStoreAPIを使用してプログラムで作成することもできます。

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

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

-Dkeystore.type パラメータを使用して、デフォルトの「JKS」(Oracle独自のキーストアプロトコル)タイプをオーバーライドできます。

-Dkeystore.type=pkcs12

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

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

3.2. 初期化

最初に、キーストアをロードする必要があります。

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

We use load whether we’re creating a new keystore or opening up an existing one. We’ll tell KeyStore to create a new one by passing null as the first parameter.

また、将来キーストアにアクセスするために使用されるパスワードも提供します。 これをnullに設定することもできますが、それによって秘密が公開されます。

3.3. 保管所

最後に、新しいキーストアをファイルシステムに保存します。

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

上記に示されていないのは、 getInstance load、、およびstoreがそれぞれスローするいくつかのチェック済み例外です。

4. キーストアの読み込み

キーストアをロードするには、前と同じように、最初にKeyStoreインスタンスを作成する必要があります。

This time though, we’ll specify the format, since we’re loading an existing one:

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

JVMが渡したキーストアタイプをサポートしていない場合、または開いているファイルシステム上のキーストアのタイプと一致しない場合は、KeyStoreExceptionが発生します。

java.security.KeyStoreException: KEYSTORE_TYPE not found

また、パスワードが間違っていると、 UnrecoverableKeyException:が発生します。

java.security.UnrecoverableKeyException: Password verification failed

5. エントリの保存

In the keystore, we can store three different kinds of entries, each 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)
  • 信頼できる証明書

それぞれを見てみましょう。

5.1. 対称鍵の保存

キーストアに保存できる最も簡単なものは、対称鍵です。

対称鍵を保存するには、次の3つが必要です。

  1. an alias – this 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 can’t be null; however, it can be an empty String. パスワードを残しておけばヌルエントリの場合、 KeyStoreException:

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

キーとパスワードをラッパークラスでラップする必要があるのは少し奇妙に思えるかもしれません。

setEntry は他のエントリタイプにも使用できる汎用メソッドであるため、キーをラップします。 エントリのタイプにより、 KeyStoreAPIはそれを異なる方法で処理できます。

KeyStore APIは、エンドユーザーからパスワードを収集するためのGUIおよびCLIへのコールバックをサポートしているため、パスワードをラップします。 We can 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. 秘密鍵の保存

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

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 – like before
  2. a private key – because we aren’t using the generic method, the key won’t get wrapped. Also, in our case, it should be an instance of PrivateKey.
  3. a password – used to access the entry. This time, the password is mandatory.
  4. a certificate chain – this certifies the corresponding public key
X509Certificate[] certificateChain = new X509Certificate[2];
chain[0] = clientCert;
chain[1] = caCert;
ks.setKeyEntry("sso-signing-key", privateKey, pwdArray, certificateChain);

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, which occurs if pwdArray is an empty array:

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

更新するには、同じエイリアスと新しいprivateKeyおよびcertificateChainを使用してメソッドを再度呼び出すだけです。

It might also be valuable to do a quick refresher on how to generate a certificate chain.

5.3. 信頼できる証明書の保存

信頼できる証明書の保存は非常に簡単です。 エイリアスと証明書自体のみが必要です 、タイプ証明書

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

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

そのため、ここで重要なのは、KeyStoreは実際にはこの証明書を検証しないということです。 保存する前に、自分で確認する必要があります。

更新するには、同じエイリアスと新しいtrustedCertificateを使用してメソッドを再度呼び出すだけです。

6. エントリを読む

いくつかのエントリを作成したので、必ずそれらを読みたいと思います。

6.1. 単一のエントリを読む

まず、エイリアスによってキーと証明書を引き出すことができます。

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

If there’s no entry by that name, or it’s 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. キーストアにエイリアスが含まれているかどうかの確認

KeyStore は、 Map を使用してエントリを格納するだけなので、エントリを取得せずに存在を確認する機能を公開します。

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. エントリーの種類を確認する

KeyStore#entryInstanceOf is a bit more powerful.

It’s similar to 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. エントリの削除

KeyStore はもちろん、は追加したエントリの削除をサポートしています。

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. キーストアの削除

キーストアを削除したい場合、APIは役に立ちませんが、Javaを使用して削除することはできます。

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. 結論

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

この例の完全な実装は、Githubにあります。