Перейти к основному содержимому

API хранилища ключей Java

· 7 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим управление криптографическими ключами и сертификатами в Java с помощью KeyStore API.

2. Хранилища ключей

Если нам нужно управлять ключами и сертификатами в Java, нам нужно хранилище ключей , которое представляет собой просто защищенную коллекцию записей ключей и сертификатов с псевдонимами.

Обычно мы сохраняем хранилища ключей в файловой системе и можем защитить их паролем.

По умолчанию в Java есть файл хранилища ключей, расположенный по адресу JAVA_HOME/ jre /lib/security/cacerts . Мы можем получить доступ к этому хранилищу ключей, используя пароль хранилища ключей по умолчанию changeit .

Теперь, с этой небольшой предысторией, давайте приступим к созданию нашего первого.

3. Создание хранилища ключей

3.1. Строительство

Мы можем легко создать хранилище ключей с помощью keytool или сделать это программно с помощью KeyStore API:

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

Здесь мы используем тип по умолчанию, хотя есть несколько доступных типов хранилища ключей, таких как jceks или pkcs12 .

Мы можем переопределить тип по умолчанию «JKS» (собственный протокол хранилища ключей Oracle), используя параметр -Dkeystore.type :

-Dkeystore.type=pkcs12

Или мы можем, конечно, указать один из поддерживаемых форматов в getInstance :

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

3.2. Инициализация

Изначально нам нужно загрузить хранилище ключей:

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

Мы используем load независимо от того, создаем ли мы новое хранилище ключей или открываем существующее.

И мы говорим KeyStore создать новый, передав null в качестве первого параметра.

Мы также предоставляем пароль, который будет использоваться для доступа к хранилищу ключей в будущем. Мы также можем установить это значение в null , хотя это раскроет наши секреты.

3.3. Хранилище

Наконец, мы сохраняем наше новое хранилище ключей в файловой системе:

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

Обратите внимание, что выше не показаны несколько проверенных исключений, которые getInstance загружают и сохраняют каждый бросок.

4. Загрузка хранилища ключей

Чтобы загрузить хранилище ключей, нам сначала нужно создать экземпляр KeyStore , как и раньше.

Однако на этот раз давайте укажем формат, так как мы загружаем существующий:

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. Хранение записей

В хранилище ключей мы можем хранить три разных типа записей, каждая запись под своим псевдонимом:

  • Симметричные ключи (называемые секретными ключами в JCE),
  • Асимметричные ключи (называемые открытыми и закрытыми ключами в JCE) и
  • Доверенные сертификаты

Давайте посмотрим на каждый из них.

5.1. Сохранение симметричного ключа

Самое простое, что мы можем хранить в хранилище ключей, — это симметричный ключ.

Чтобы сохранить симметричный ключ, нам понадобятся три вещи:

  1. псевдоним — это просто имя, которое мы будем использовать в будущем для ссылки на запись
  2. ключ , заключенный в KeyStore.SecretKeyEntry .
  3. пароль , заключенный в то, что называется ProtectionParam .
KeyStore.SecretKeyEntry secret
= new KeyStore.SecretKeyEntry(secretKey);
KeyStore.ProtectionParameter password
= new KeyStore.PasswordProtection(pwdArray);
ks.setEntry("db-encryption-secret", secret, password);

Имейте в виду, что пароль не может быть нулевым, однако он может быть пустой строкой. Если мы оставим пароль нулевым для записи, мы получим KeyStoreException:

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

Может показаться немного странным, что нам нужно обернуть ключ и пароль в классы-оболочки.

Мы обертываем ключ, потому что setEntry — это общий метод, который можно использовать и для других типов записей. Тип записи позволяет KeyStore API обрабатывать ее по-разному.

Мы упаковываем пароль, потому что KeyStore API поддерживает обратные вызовы для графических интерфейсов и командной строки для сбора пароля от конечного пользователя. Дополнительные сведения см . в Javadoc KeyStore .CallbackHandlerProtection .

Мы также можем использовать этот метод для обновления существующего ключа. Нам просто нужно снова вызвать его с тем же псевдонимом, паролем и нашим новым секретом.

5.2. Сохранение закрытого ключа

Хранение асимметричных ключей немного сложнее, так как нам нужно иметь дело с цепочками сертификатов.

Кроме того, KeyStore API предоставляет специальный метод setKeyEntry , который более удобен, чем общий метод setEntry .

Итак, чтобы сохранить асимметричный ключ, нам понадобятся четыре вещи:

  1. псевдоним , такой же, как и раньше
  2. закрытый ключ . Поскольку мы не используем общий метод, ключ не будет упакован. Также для нашего случая это должен быть экземпляр PrivateKey
  3. пароль для доступа к записи. На этот раз пароль обязателен
  4. цепочка сертификатов, которая удостоверяет соответствующий открытый ключ
X509Certificate[] certificateChain = new X509Certificate[2];
chain[0] = clientCert;
chain[1] = caCert;
ks.setKeyEntry("sso-signing-key", privateKey, pwdArray, certificateChain);

Конечно, здесь многое может пойти не так, например, если pwdArray имеет значение null :

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

Но есть действительно странное исключение, о котором следует знать, и это если pwdArray является пустым массивом:

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

Чтобы обновить, мы можем просто снова вызвать метод с тем же псевдонимом и новым privateKey и CertificateChain.

Кроме того, может быть полезно быстро напомнить, как создать цепочку сертификатов .

5.3. Сохранение доверенного сертификата

Хранить доверенные сертификаты довольно просто. Для этого требуется только псевдоним и сам сертификат `` , который имеет тип Certificate :

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

Обычно сертификат создается не нами, а сторонним поставщиком.

По этой причине здесь важно отметить, что KeyStore на самом деле не проверяет этот сертификат. Мы должны проверить его самостоятельно перед сохранением.

Для обновления мы можем просто снова вызвать метод с тем же псевдонимом и новым trustCertificate .

6. Чтение записей

Теперь, когда мы написали несколько записей, мы, безусловно, захотим их прочитать.

6.1. Чтение одной записи

Во-первых, мы можем получить ключи и сертификаты по их псевдонимам:

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

Если записи с таким именем нет или она другого типа, то getKey просто возвращает 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"));
}

Но если пароль для ключа неверный, мы получим ту же странную ошибку, о которой говорили ранее:

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 немного мощнее.

Это похоже на containsAlias , за исключением того, что он также проверяет тип записи:

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);
}

К счастью, deleteEntry является идемпотентным, поэтому метод реагирует одинаково независимо от того, существует запись или нет.

8. Удаление хранилища ключей

Если мы хотим удалить наше хранилище ключей, API нам не поможет, но мы все равно можем использовать для этого Java:

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

Или, в качестве альтернативы, мы можем сохранить хранилище ключей и просто удалить записи:

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

9. Заключение

В этой статье мы рассказали об управлении сертификатами и ключами с помощью KeyStore API . Мы обсудили, что такое хранилище ключей, как его создать, загрузить и удалить, как сохранить ключ или сертификат в хранилище ключей и как загрузить и обновить существующие записи новыми значениями.

Полную реализацию примера можно найти на Github .