Skip to content

Commit fc09896

Browse files
authored
Remove keystore v1 and v2 formats (#87893)
The keystore format has been changed a few times since it was first introduced. Part of Elasticsearch startup automatically upgrades the format. Since Elasticsearch has fixed bounds of supported versions for upgrades, there are also fixed bounds on the keystore formats we might need to read. The v3 keystore format was introduced in Elasticsearch 6.3.0. Since current Elasticsearch master branch is 8.x, and 8.x only supports offline upgrades from 7.x, it is therefore impossible to need to read v1 or v2 formats. This commit removes support for those formats.
1 parent f627bdd commit fc09896

File tree

2 files changed

+7
-229
lines changed

2 files changed

+7
-229
lines changed

distribution/tools/keystore-cli/src/test/java/org/elasticsearch/cli/keystore/KeyStoreWrapperTests.java

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,10 @@
3434
import java.nio.file.Files;
3535
import java.nio.file.Path;
3636
import java.security.GeneralSecurityException;
37-
import java.security.KeyStore;
3837
import java.security.MessageDigest;
3938
import java.security.SecureRandom;
4039
import java.util.ArrayList;
4140
import java.util.Arrays;
42-
import java.util.Base64;
4341
import java.util.List;
4442
import java.util.Locale;
4543
import java.util.Set;
@@ -375,98 +373,6 @@ public void testIllegalSettingName() throws Exception {
375373
assertTrue(e.getMessage().contains("does not match the allowed setting name pattern"));
376374
}
377375

378-
public void testBackcompatV1() throws Exception {
379-
assumeFalse("Can't run in a FIPS JVM as PBE is not available", inFipsJvm());
380-
Path configDir = env.configFile();
381-
try (
382-
Directory directory = newFSDirectory(configDir);
383-
IndexOutput output = EndiannessReverserUtil.createOutput(directory, "elasticsearch.keystore", IOContext.DEFAULT);
384-
) {
385-
CodecUtil.writeHeader(output, "elasticsearch.keystore", 1);
386-
output.writeByte((byte) 0); // hasPassword = false
387-
output.writeString("PKCS12");
388-
output.writeString("PBE");
389-
390-
SecretKeyFactory secretFactory = SecretKeyFactory.getInstance("PBE");
391-
KeyStore keystore = KeyStore.getInstance("PKCS12");
392-
keystore.load(null, null);
393-
SecretKey secretKey = secretFactory.generateSecret(new PBEKeySpec("stringSecretValue".toCharArray()));
394-
KeyStore.ProtectionParameter protectionParameter = new KeyStore.PasswordProtection(new char[0]);
395-
keystore.setEntry("string_setting", new KeyStore.SecretKeyEntry(secretKey), protectionParameter);
396-
397-
ByteArrayOutputStream keystoreBytesStream = new ByteArrayOutputStream();
398-
keystore.store(keystoreBytesStream, new char[0]);
399-
byte[] keystoreBytes = keystoreBytesStream.toByteArray();
400-
output.writeInt(keystoreBytes.length);
401-
output.writeBytes(keystoreBytes, keystoreBytes.length);
402-
CodecUtil.writeFooter(output);
403-
}
404-
405-
KeyStoreWrapper keystore = KeyStoreWrapper.load(configDir);
406-
keystore.decrypt(new char[0]);
407-
SecureString testValue = keystore.getString("string_setting");
408-
assertThat(testValue.toString(), equalTo("stringSecretValue"));
409-
}
410-
411-
public void testBackcompatV2() throws Exception {
412-
assumeFalse("Can't run in a FIPS JVM as PBE is not available", inFipsJvm());
413-
Path configDir = env.configFile();
414-
byte[] fileBytes = new byte[20];
415-
random().nextBytes(fileBytes);
416-
try (
417-
Directory directory = newFSDirectory(configDir);
418-
IndexOutput output = EndiannessReverserUtil.createOutput(directory, "elasticsearch.keystore", IOContext.DEFAULT);
419-
) {
420-
CodecUtil.writeHeader(output, "elasticsearch.keystore", KeyStoreWrapper.V2_VERSION);
421-
output.writeByte((byte) 0); // hasPassword = false
422-
output.writeString("PKCS12");
423-
output.writeString("PBE"); // string algo
424-
output.writeString("PBE"); // file algo
425-
426-
output.writeVInt(2); // num settings
427-
output.writeString("string_setting");
428-
output.writeString("STRING");
429-
output.writeString("file_setting");
430-
output.writeString("FILE");
431-
432-
SecretKeyFactory secretFactory = SecretKeyFactory.getInstance("PBE");
433-
KeyStore keystore = KeyStore.getInstance("PKCS12");
434-
keystore.load(null, null);
435-
SecretKey secretKey = secretFactory.generateSecret(new PBEKeySpec("stringSecretValue".toCharArray()));
436-
KeyStore.ProtectionParameter protectionParameter = new KeyStore.PasswordProtection(new char[0]);
437-
keystore.setEntry("string_setting", new KeyStore.SecretKeyEntry(secretKey), protectionParameter);
438-
439-
byte[] base64Bytes = Base64.getEncoder().encode(fileBytes);
440-
char[] chars = new char[base64Bytes.length];
441-
for (int i = 0; i < chars.length; ++i) {
442-
chars[i] = (char) base64Bytes[i]; // PBE only stores the lower 8 bits, so this narrowing is ok
443-
}
444-
secretKey = secretFactory.generateSecret(new PBEKeySpec(chars));
445-
keystore.setEntry("file_setting", new KeyStore.SecretKeyEntry(secretKey), protectionParameter);
446-
447-
ByteArrayOutputStream keystoreBytesStream = new ByteArrayOutputStream();
448-
keystore.store(keystoreBytesStream, new char[0]);
449-
byte[] keystoreBytes = keystoreBytesStream.toByteArray();
450-
output.writeInt(keystoreBytes.length);
451-
output.writeBytes(keystoreBytes, keystoreBytes.length);
452-
CodecUtil.writeFooter(output);
453-
}
454-
455-
KeyStoreWrapper keystore = KeyStoreWrapper.load(configDir);
456-
keystore.decrypt(new char[0]);
457-
SecureString testValue = keystore.getString("string_setting");
458-
assertThat(testValue.toString(), equalTo("stringSecretValue"));
459-
460-
try (InputStream fileInput = keystore.getFile("file_setting")) {
461-
byte[] readBytes = new byte[20];
462-
assertEquals(20, fileInput.read(readBytes));
463-
for (int i = 0; i < fileBytes.length; ++i) {
464-
assertThat("byte " + i, readBytes[i], equalTo(fileBytes[i]));
465-
}
466-
assertEquals(-1, fileInput.read());
467-
}
468-
}
469-
470376
public void testBackcompatV4() throws Exception {
471377
assumeFalse("Can't run in a FIPS JVM as PBE is not available", inFipsJvm());
472378
Path configDir = env.configFile();

server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java

Lines changed: 7 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,9 @@
4343
import java.nio.file.attribute.PosixFileAttributeView;
4444
import java.nio.file.attribute.PosixFilePermissions;
4545
import java.security.GeneralSecurityException;
46-
import java.security.KeyStore;
4746
import java.security.SecureRandom;
4847
import java.util.Arrays;
49-
import java.util.Base64;
50-
import java.util.Enumeration;
5148
import java.util.HashMap;
52-
import java.util.HashSet;
5349
import java.util.Locale;
5450
import java.util.Map;
5551
import java.util.Set;
@@ -109,7 +105,7 @@ private static class Entry {
109105
public static final String KEYSTORE_FILENAME = "elasticsearch.keystore";
110106

111107
/** The oldest metadata format version that can be read. */
112-
private static final int MIN_FORMAT_VERSION = 1;
108+
private static final int MIN_FORMAT_VERSION = 3;
113109
/** Legacy versions of the metadata written before the keystore data. */
114110
public static final int V2_VERSION = 2;
115111
public static final int V3_VERSION = 3;
@@ -268,59 +264,15 @@ public static KeyStoreWrapper load(Path configDir) throws IOException {
268264
throw new IllegalStateException("hasPassword boolean is corrupt: " + String.format(Locale.ROOT, "%02x", hasPasswordByte));
269265
}
270266

271-
if (formatVersion <= V2_VERSION) {
272-
String type = input.readString();
273-
if (type.equals("PKCS12") == false) {
274-
throw new IllegalStateException("Corrupted legacy keystore string encryption algorithm");
275-
}
276-
277-
final String stringKeyAlgo = input.readString();
278-
if (stringKeyAlgo.equals("PBE") == false) {
279-
throw new IllegalStateException("Corrupted legacy keystore string encryption algorithm");
280-
}
281-
if (formatVersion == V2_VERSION) {
282-
final String fileKeyAlgo = input.readString();
283-
if (fileKeyAlgo.equals("PBE") == false) {
284-
throw new IllegalStateException("Corrupted legacy keystore file encryption algorithm");
285-
}
286-
}
287-
}
288-
289267
final byte[] dataBytes;
290-
if (formatVersion == V2_VERSION) {
291-
// For v2 we had a map of strings containing the types for each setting. In v3 this map is now
292-
// part of the encrypted bytes. Unfortunately we cannot seek backwards with checksum input, so
293-
// we cannot just read the map and find out how long it is. So instead we read the map and
294-
// store it back using java's builtin DataOutput in a byte array, along with the actual keystore bytes
295-
Map<String, String> settingTypes = input.readMapOfStrings();
296-
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
297-
try (DataOutputStream output = new DataOutputStream(bytes)) {
298-
output.writeInt(settingTypes.size());
299-
for (Map.Entry<String, String> entry : settingTypes.entrySet()) {
300-
output.writeUTF(entry.getKey());
301-
output.writeUTF(entry.getValue());
302-
}
303-
final int keystoreLen;
304-
if (formatVersion < LE_VERSION) {
305-
keystoreLen = Integer.reverseBytes(input.readInt());
306-
} else {
307-
keystoreLen = input.readInt();
308-
}
309-
byte[] keystoreBytes = new byte[keystoreLen];
310-
input.readBytes(keystoreBytes, 0, keystoreLen);
311-
output.write(keystoreBytes);
312-
}
313-
dataBytes = bytes.toByteArray();
268+
int dataBytesLen;
269+
if (formatVersion < LE_VERSION) {
270+
dataBytesLen = Integer.reverseBytes(input.readInt());
314271
} else {
315-
int dataBytesLen;
316-
if (formatVersion < LE_VERSION) {
317-
dataBytesLen = Integer.reverseBytes(input.readInt());
318-
} else {
319-
dataBytesLen = input.readInt();
320-
}
321-
dataBytes = new byte[dataBytesLen];
322-
input.readBytes(dataBytes, 0, dataBytesLen);
272+
dataBytesLen = input.readInt();
323273
}
274+
dataBytes = new byte[dataBytesLen];
275+
input.readBytes(dataBytes, 0, dataBytesLen);
324276

325277
CodecUtil.checkFooter(input);
326278
return new KeyStoreWrapper(formatVersion, hasPassword, dataBytes);
@@ -378,13 +330,6 @@ public void decrypt(char[] password) throws GeneralSecurityException, IOExceptio
378330
if (entries.get() != null) {
379331
throw new IllegalStateException("Keystore has already been decrypted");
380332
}
381-
if (formatVersion <= V2_VERSION) {
382-
decryptLegacyEntries();
383-
if (password.length != 0) {
384-
throw new IllegalArgumentException("Keystore format does not accept non-empty passwords");
385-
}
386-
return;
387-
}
388333

389334
final byte[] salt;
390335
final byte[] iv;
@@ -462,79 +407,6 @@ private byte[] encrypt(char[] password, byte[] salt, byte[] iv) throws GeneralSe
462407
return bytes.toByteArray();
463408
}
464409

465-
private void decryptLegacyEntries() throws GeneralSecurityException, IOException {
466-
// v1 and v2 keystores never had passwords actually used, so we always use an empty password
467-
KeyStore keystore = KeyStore.getInstance("PKCS12");
468-
Map<String, EntryType> settingTypes = new HashMap<>();
469-
ByteArrayInputStream inputBytes = new ByteArrayInputStream(dataBytes);
470-
try (DataInputStream input = new DataInputStream(inputBytes)) {
471-
// first read the setting types map
472-
if (formatVersion == V2_VERSION) {
473-
int numSettings = input.readInt();
474-
for (int i = 0; i < numSettings; ++i) {
475-
String key = input.readUTF();
476-
String value = input.readUTF();
477-
settingTypes.put(key, EntryType.valueOf(value));
478-
}
479-
}
480-
// then read the actual keystore
481-
keystore.load(input, "".toCharArray());
482-
}
483-
484-
// verify the settings metadata matches the keystore entries
485-
Enumeration<String> aliases = keystore.aliases();
486-
if (formatVersion == MIN_FORMAT_VERSION) {
487-
while (aliases.hasMoreElements()) {
488-
settingTypes.put(aliases.nextElement(), EntryType.STRING);
489-
}
490-
} else {
491-
// verify integrity: keys in keystore match what the metadata thinks exist
492-
Set<String> expectedSettings = new HashSet<>(settingTypes.keySet());
493-
while (aliases.hasMoreElements()) {
494-
String settingName = aliases.nextElement();
495-
if (expectedSettings.remove(settingName) == false) {
496-
throw new SecurityException("Keystore has been corrupted or tampered with");
497-
}
498-
}
499-
if (expectedSettings.isEmpty() == false) {
500-
throw new SecurityException("Keystore has been corrupted or tampered with");
501-
}
502-
}
503-
504-
// fill in the entries now that we know all the types to expect
505-
this.entries.set(new HashMap<>());
506-
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBE");
507-
KeyStore.PasswordProtection password = new KeyStore.PasswordProtection("".toCharArray());
508-
509-
for (Map.Entry<String, EntryType> settingEntry : settingTypes.entrySet()) {
510-
String setting = settingEntry.getKey();
511-
EntryType settingType = settingEntry.getValue();
512-
KeyStore.SecretKeyEntry keystoreEntry = (KeyStore.SecretKeyEntry) keystore.getEntry(setting, password);
513-
PBEKeySpec keySpec = (PBEKeySpec) keyFactory.getKeySpec(keystoreEntry.getSecretKey(), PBEKeySpec.class);
514-
char[] chars = keySpec.getPassword();
515-
keySpec.clearPassword();
516-
517-
final byte[] bytes;
518-
if (settingType == EntryType.STRING) {
519-
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(chars));
520-
bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
521-
Arrays.fill(byteBuffer.array(), (byte) 0);
522-
} else {
523-
assert settingType == EntryType.FILE;
524-
// The PBE keyspec gives us chars, we convert to bytes
525-
byte[] tmpBytes = new byte[chars.length];
526-
for (int i = 0; i < tmpBytes.length; ++i) {
527-
tmpBytes[i] = (byte) chars[i]; // PBE only stores the lower 8 bits, so this narrowing is ok
528-
}
529-
bytes = Base64.getDecoder().decode(tmpBytes);
530-
Arrays.fill(tmpBytes, (byte) 0);
531-
}
532-
Arrays.fill(chars, '\0');
533-
534-
entries.get().put(setting, new Entry(bytes));
535-
}
536-
}
537-
538410
/** Write the keystore to the given config directory. */
539411
public synchronized void save(Path configDir, char[] password) throws Exception {
540412
save(configDir, password, true);

0 commit comments

Comments
 (0)