diff --git a/x-pack/license-tools/src/main/java/org/elasticsearch/license/licensor/LicenseSigner.java b/x-pack/license-tools/src/main/java/org/elasticsearch/license/licensor/LicenseSigner.java
index 1b28878e888..a2b8dc9908f 100644
--- a/x-pack/license-tools/src/main/java/org/elasticsearch/license/licensor/LicenseSigner.java
+++ b/x-pack/license-tools/src/main/java/org/elasticsearch/license/licensor/LicenseSigner.java
@@ -8,6 +8,7 @@ package org.elasticsearch.license.licensor;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefIterator;
import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
@@ -20,7 +21,10 @@ import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
+import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
@@ -35,9 +39,7 @@ import java.util.Map;
public class LicenseSigner {
private static final int MAGIC_LENGTH = 13;
-
private final Path publicKeyPath;
-
private final Path privateKeyPath;
public LicenseSigner(final Path privateKeyPath, final Path publicKeyPath) {
@@ -59,9 +61,11 @@ public class LicenseSigner {
Collections.singletonMap(License.LICENSE_SPEC_VIEW_MODE, "true");
licenseSpec.toXContent(contentBuilder, new ToXContent.MapParams(licenseSpecViewMode));
final byte[] signedContent;
+ final boolean preV4 = licenseSpec.version() < License.VERSION_CRYPTO_ALGORITHMS;
try {
final Signature rsa = Signature.getInstance("SHA512withRSA");
- rsa.initSign(CryptUtils.readEncryptedPrivateKey(Files.readAllBytes(privateKeyPath)));
+ PrivateKey decryptedPrivateKey = CryptUtils.readEncryptedPrivateKey(Files.readAllBytes(privateKeyPath));
+ rsa.initSign(decryptedPrivateKey);
final BytesRefIterator iterator = BytesReference.bytes(contentBuilder).iterator();
BytesRef ref;
while((ref = iterator.next()) != null) {
@@ -77,15 +81,17 @@ public class LicenseSigner {
final byte[] magic = new byte[MAGIC_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(magic);
- final byte[] hash = Base64.getEncoder().encode(Files.readAllBytes(publicKeyPath));
- assert hash != null;
- byte[] bytes = new byte[4 + 4 + MAGIC_LENGTH + 4 + hash.length + 4 + signedContent.length];
+ final byte[] publicKeyBytes = Files.readAllBytes(publicKeyPath);
+ PublicKey publicKey = CryptUtils.readPublicKey(publicKeyBytes);
+ final byte[] pubKeyFingerprint = preV4 ? Base64.getEncoder().encode(CryptUtils.writeEncryptedPublicKey(publicKey)) :
+ getPublicKeyFingerprint(publicKeyBytes);
+ byte[] bytes = new byte[4 + 4 + MAGIC_LENGTH + 4 + pubKeyFingerprint.length + 4 + signedContent.length];
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.putInt(licenseSpec.version())
.putInt(magic.length)
.put(magic)
- .putInt(hash.length)
- .put(hash)
+ .putInt(pubKeyFingerprint.length)
+ .put(pubKeyFingerprint)
.putInt(signedContent.length)
.put(signedContent);
@@ -93,4 +99,10 @@ public class LicenseSigner {
.fromLicenseSpec(licenseSpec, Base64.getEncoder().encodeToString(bytes))
.build();
}
+
+ private byte[] getPublicKeyFingerprint(byte[] keyBytes) {
+ MessageDigest sha256 = MessageDigests.sha256();
+ sha256.update(keyBytes);
+ return sha256.digest();
+ }
}
diff --git a/x-pack/license-tools/src/test/resources/private.key b/x-pack/license-tools/src/test/resources/private.key
index 1f545803d87..d0a658c3f07 100644
Binary files a/x-pack/license-tools/src/test/resources/private.key and b/x-pack/license-tools/src/test/resources/private.key differ
diff --git a/x-pack/license-tools/src/test/resources/public.key b/x-pack/license-tools/src/test/resources/public.key
index 2a9f272e0b3..a94823f5b62 100644
Binary files a/x-pack/license-tools/src/test/resources/public.key and b/x-pack/license-tools/src/test/resources/public.key differ
diff --git a/x-pack/plugin/core/snapshot.key b/x-pack/plugin/core/snapshot.key
index 2a9f272e0b3..a94823f5b62 100644
Binary files a/x-pack/plugin/core/snapshot.key and b/x-pack/plugin/core/snapshot.key differ
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/CryptUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/CryptUtils.java
index c3c69d5bfcd..8f55cca1a84 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/CryptUtils.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/CryptUtils.java
@@ -15,95 +15,71 @@ import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
+import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
+import java.security.NoSuchAlgorithmException;
+import java.security.InvalidKeyException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class CryptUtils {
- private static final int minimumPadding = 20;
- private static final byte[] salt = {
- (byte) 0xA9, (byte) 0xA2, (byte) 0xB5, (byte) 0xDE,
- (byte) 0x2A, (byte) 0x8A, (byte) 0x9A, (byte) 0xE6
+ // SALT must be at least 128bits for FIPS 140-2 compliance
+ private static final byte[] SALT = {
+ (byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73,
+ (byte) 0x69, (byte) 0x73, (byte) 0x74, (byte) 0x68,
+ (byte) 0x65, (byte) 0x73, (byte) 0x61, (byte) 0x6C,
+ (byte) 0x74, (byte) 0x77, (byte) 0x65, (byte) 0x75
};
- private static final int iterationCount = 1024;
- private static final int aesKeyLength = 128;
- private static final String keyAlgorithm = "RSA";
- private static final String passHashAlgorithm = "SHA-512";
- private static final String DEFAULT_PASS_PHRASE = "elasticsearch-license";
-
- private static final SecureRandom random = new SecureRandom();
+ private static final String KEY_ALGORITHM = "RSA";
+ private static final char[] DEFAULT_PASS_PHRASE = "elasticsearch-license".toCharArray();
+ private static final String KDF_ALGORITHM = "PBKDF2WithHmacSHA512";
+ private static final int KDF_ITERATION_COUNT = 10000;
+ private static final String CIPHER_ALGORITHM = "AES";
+ // This can be changed to 256 once Java 9 is the minimum version
+ // http://www.oracle.com/technetwork/java/javase/terms/readme/jdk9-readme-3852447.html#jce
+ private static final int ENCRYPTION_KEY_LENGTH = 128;
+ private static final SecureRandom RANDOM = new SecureRandom();
/**
* Read encrypted private key file content with default pass phrase
*/
public static PrivateKey readEncryptedPrivateKey(byte[] fileContents) {
- try {
- return readEncryptedPrivateKey(fileContents, hashPassPhrase(DEFAULT_PASS_PHRASE));
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException(e);
- }
- }
-
- /**
- * Read encrypted public key file content with default pass phrase
- */
- public static PublicKey readEncryptedPublicKey(byte[] fileContents) {
- try {
- return readEncryptedPublicKey(fileContents, hashPassPhrase(DEFAULT_PASS_PHRASE));
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException(e);
- }
- }
-
- /**
- * Returns encrypted public key file content with default pass phrase
- */
- public static byte[] writeEncryptedPublicKey(PublicKey publicKey) {
- try {
- return writeEncryptedPublicKey(publicKey, hashPassPhrase(DEFAULT_PASS_PHRASE));
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException(e);
- }
+ return readEncryptedPrivateKey(fileContents, DEFAULT_PASS_PHRASE, false);
}
/**
* Returns encrypted private key file content with default pass phrase
*/
public static byte[] writeEncryptedPrivateKey(PrivateKey privateKey) {
- try {
- return writeEncryptedPrivateKey(privateKey, hashPassPhrase(DEFAULT_PASS_PHRASE));
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException(e);
- }
+ return writeEncryptedPrivateKey(privateKey, DEFAULT_PASS_PHRASE);
}
/**
* Read encrypted private key file content with provided passPhrase
*/
- public static PrivateKey readEncryptedPrivateKey(byte[] fileContents, char[] passPhrase) {
- PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(decrypt(fileContents, passPhrase));
+ public static PrivateKey readEncryptedPrivateKey(byte[] fileContents, char[] passPhrase, boolean preV4) {
+ byte[] keyBytes = preV4 ? decryptV3Format(fileContents) : decrypt(fileContents, passPhrase);
+ PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(keyBytes);
try {
- return KeyFactory.getInstance(keyAlgorithm).generatePrivate(privateKeySpec);
+ return KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(privateKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IllegalStateException(e);
}
}
/**
- * Read encrypted public key file content with provided passPhrase
+ * Read public key file content
*/
- public static PublicKey readEncryptedPublicKey(byte[] fileContents, char[] passPhrase) {
- X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(decrypt(fileContents, passPhrase));
+ public static PublicKey readPublicKey(byte[] fileContents) {
+ X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(fileContents);
try {
- return KeyFactory.getInstance(CryptUtils.keyAlgorithm).generatePublic(publicKeySpec);
+ return KeyFactory.getInstance(CryptUtils.KEY_ALGORITHM).generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IllegalStateException(e);
}
@@ -112,9 +88,9 @@ public class CryptUtils {
/**
* Returns encrypted public key file content with provided passPhrase
*/
- public static byte[] writeEncryptedPublicKey(PublicKey publicKey, char[] passPhrase) {
+ public static byte[] writeEncryptedPublicKey(PublicKey publicKey) {
X509EncodedKeySpec encodedKeySpec = new X509EncodedKeySpec(publicKey.getEncoded());
- return encrypt(encodedKeySpec.getEncoded(), passPhrase);
+ return encrypt(encodedKeySpec.getEncoded(), DEFAULT_PASS_PHRASE);
}
/**
@@ -128,33 +104,25 @@ public class CryptUtils {
/**
* Encrypts provided data
with DEFAULT_PASS_PHRASE
*/
- public static byte[] encrypt(byte[] data) {
- try {
- return encrypt(data, hashPassPhrase(DEFAULT_PASS_PHRASE));
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException(e);
- }
+ static byte[] encrypt(byte[] data) {
+ return encrypt(data, DEFAULT_PASS_PHRASE);
}
/**
* Decrypts provided encryptedData
with DEFAULT_PASS_PHRASE
*/
- public static byte[] decrypt(byte[] encryptedData) {
- try {
- return decrypt(encryptedData, hashPassPhrase(DEFAULT_PASS_PHRASE));
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException(e);
- }
+ static byte[] decrypt(byte[] encryptedData) {
+ return decrypt(encryptedData, DEFAULT_PASS_PHRASE);
}
/**
* Encrypts provided data
with passPhrase
*/
- public static byte[] encrypt(byte[] data, char[] passPhrase) {
+ private static byte[] encrypt(byte[] data, char[] passPhrase) {
try {
- final Cipher encryptionCipher = getEncryptionCipher(getSecretKey(passPhrase));
- return encryptionCipher.doFinal(pad(data, minimumPadding));
- } catch (InvalidKeySpecException | IllegalBlockSizeException | BadPaddingException e) {
+ final Cipher encryptionCipher = getEncryptionCipher(deriveSecretKey(passPhrase));
+ return encryptionCipher.doFinal(data);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
}
@@ -164,29 +132,60 @@ public class CryptUtils {
*/
private static byte[] decrypt(byte[] encryptedData, char[] passPhrase) {
try {
- final Cipher cipher = getDecryptionCipher(getSecretKey(passPhrase));
- return unPad(cipher.doFinal(encryptedData));
- } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException e) {
+ final Cipher cipher = getDecryptionCipher(deriveSecretKey(passPhrase));
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
-
}
- private static SecretKey getSecretKey(char[] passPhrase) throws InvalidKeySpecException {
+ static byte[] encryptV3Format(byte[] data) {
try {
- PBEKeySpec keySpec = new PBEKeySpec(passPhrase, salt, iterationCount, aesKeyLength);
+ SecretKey encryptionKey = getV3Key();
+ final Cipher encryptionCipher = getEncryptionCipher(encryptionKey);
+ return encryptionCipher.doFinal(pad(data, 20));
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException(e);
+ }
+ }
- byte[] shortKey = SecretKeyFactory.getInstance("PBEWithSHA1AndDESede").
- generateSecret(keySpec).getEncoded();
+ static byte[] decryptV3Format(byte[] data) {
+ try {
+ SecretKey decryptionKey = getV3Key();
+ final Cipher decryptionCipher = getDecryptionCipher(decryptionKey);
+ return unPad(decryptionCipher.doFinal(data));
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException(e);
+ }
+ }
- byte[] intermediaryKey = new byte[aesKeyLength / 8];
- for (int i = 0, j = 0; i < aesKeyLength / 8; i++) {
- intermediaryKey[i] = shortKey[j];
- if (++j == shortKey.length)
- j = 0;
- }
+ private static SecretKey getV3Key() throws NoSuchAlgorithmException, InvalidKeySpecException {
+ final byte[] salt = {
+ (byte) 0xA9, (byte) 0xA2, (byte) 0xB5, (byte) 0xDE,
+ (byte) 0x2A, (byte) 0x8A, (byte) 0x9A, (byte) 0xE6
+ };
+ final byte[] passBytes = "elasticsearch-license".getBytes(StandardCharsets.UTF_8);
+ final byte[] digest = MessageDigest.getInstance("SHA-512").digest(passBytes);
+ final char[] hashedPassphrase = Base64.getEncoder().encodeToString(digest).toCharArray();
+ PBEKeySpec keySpec = new PBEKeySpec(hashedPassphrase, salt, 1024, 128);
+ byte[] shortKey = SecretKeyFactory.getInstance("PBEWithSHA1AndDESede").
+ generateSecret(keySpec).getEncoded();
+ byte[] intermediaryKey = new byte[16];
+ for (int i = 0, j = 0; i < 16; i++) {
+ intermediaryKey[i] = shortKey[j];
+ if (++j == shortKey.length)
+ j = 0;
+ }
+ return new SecretKeySpec(intermediaryKey, "AES");
+ }
- return new SecretKeySpec(intermediaryKey, "AES");
+ private static SecretKey deriveSecretKey(char[] passPhrase) {
+ try {
+ PBEKeySpec keySpec = new PBEKeySpec(passPhrase, SALT, KDF_ITERATION_COUNT, ENCRYPTION_KEY_LENGTH);
+
+ SecretKey secretKey = SecretKeyFactory.getInstance(KDF_ALGORITHM).
+ generateSecret(keySpec);
+ return new SecretKeySpec(secretKey.getEncoded(), CIPHER_ALGORITHM);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IllegalStateException(e);
}
@@ -202,8 +201,8 @@ public class CryptUtils {
private static Cipher getCipher(int mode, SecretKey secretKey) {
try {
- Cipher cipher = Cipher.getInstance(secretKey.getAlgorithm());
- cipher.init(mode, secretKey, random);
+ Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ cipher.init(mode, secretKey, RANDOM);
return cipher;
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
@@ -228,7 +227,7 @@ public class CryptUtils {
// fill the rest with random bytes
byte[] fill = new byte[padded - 1];
- random.nextBytes(fill);
+ RANDOM.nextBytes(fill);
System.arraycopy(fill, 0, out, i, padded - 1);
out[length] = (byte) (padded + 1);
@@ -246,10 +245,4 @@ public class CryptUtils {
return out;
}
-
- private static char[] hashPassPhrase(String passPhrase) throws NoSuchAlgorithmException {
- final byte[] passBytes = passPhrase.getBytes(StandardCharsets.UTF_8);
- final byte[] digest = MessageDigest.getInstance(passHashAlgorithm).digest(passBytes);
- return Base64.getEncoder().encodeToString(digest).toCharArray();
- }
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java
index df94a9132a0..144eec96858 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java
@@ -38,7 +38,8 @@ public class License implements ToXContentObject {
public static final int VERSION_START = 1;
public static final int VERSION_NO_FEATURE_TYPE = 2;
public static final int VERSION_START_DATE = 3;
- public static final int VERSION_CURRENT = VERSION_START_DATE;
+ public static final int VERSION_CRYPTO_ALGORITHMS = 4;
+ public static final int VERSION_CURRENT = VERSION_CRYPTO_ALGORITHMS;
/**
* XContent param name to deserialize license(s) with
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java
index 14e72142715..fa0c239aab1 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java
@@ -402,9 +402,9 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
boolean noLicense = noLicenseInPrevMetadata && noLicenseInCurrentMetadata;
// auto-generate license if no licenses ever existed or if the current license is basic and
- // needs extended. this will trigger a subsequent cluster changed event
- if (currentClusterState.getNodes().isLocalNodeElectedMaster()
- && (noLicense || LicenseUtils.licenseNeedsExtended(currentLicense))) {
+ // needs extended or if the license signature needs to be updated. this will trigger a subsequent cluster changed event
+ if (currentClusterState.getNodes().isLocalNodeElectedMaster() &&
+ (noLicense || LicenseUtils.licenseNeedsExtended(currentLicense) || LicenseUtils.signatureNeedsUpdate(currentLicense))) {
registerOrUpdateSelfGeneratedLicense();
}
} else if (logger.isDebugEnabled()) {
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseUtils.java
index c5ab35f862c..8fcdc05bcf9 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseUtils.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseUtils.java
@@ -37,4 +37,13 @@ public class LicenseUtils {
public static boolean licenseNeedsExtended(License license) {
return "basic".equals(license.type()) && license.expiryDate() != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
}
+
+ /**
+ * Checks if the signature of a self generated license with older version needs to be
+ * recreated with the new key
+ */
+ public static boolean signatureNeedsUpdate(License license) {
+ return ("basic".equals(license.type()) || "trial".equals(license.type())) &&
+ (license.version() < License.VERSION_CRYPTO_ALGORITHMS);
+ }
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseVerifier.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseVerifier.java
index c670f070ad7..a879dc9ed18 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseVerifier.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseVerifier.java
@@ -37,9 +37,9 @@ public class LicenseVerifier {
* @param license to verify
* @return true if valid, false otherwise
*/
- public static boolean verifyLicense(final License license, byte[] encryptedPublicKeyData) {
+ public static boolean verifyLicense(final License license, byte[] publicKeyData) {
byte[] signedContent = null;
- byte[] signatureHash = null;
+ byte[] publicKeyFingerprint = null;
try {
byte[] signatureBytes = Base64.getDecoder().decode(license.signature());
ByteBuffer byteBuffer = ByteBuffer.wrap(signatureBytes);
@@ -48,32 +48,27 @@ public class LicenseVerifier {
byte[] magic = new byte[magicLen];
byteBuffer.get(magic);
int hashLen = byteBuffer.getInt();
- signatureHash = new byte[hashLen];
- byteBuffer.get(signatureHash);
+ publicKeyFingerprint = new byte[hashLen];
+ byteBuffer.get(publicKeyFingerprint);
int signedContentLen = byteBuffer.getInt();
signedContent = new byte[signedContentLen];
byteBuffer.get(signedContent);
XContentBuilder contentBuilder = XContentFactory.contentBuilder(XContentType.JSON);
license.toXContent(contentBuilder, new ToXContent.MapParams(Collections.singletonMap(License.LICENSE_SPEC_VIEW_MODE, "true")));
Signature rsa = Signature.getInstance("SHA512withRSA");
- rsa.initVerify(CryptUtils.readEncryptedPublicKey(encryptedPublicKeyData));
+ rsa.initVerify(CryptUtils.readPublicKey(publicKeyData));
BytesRefIterator iterator = BytesReference.bytes(contentBuilder).iterator();
BytesRef ref;
while((ref = iterator.next()) != null) {
rsa.update(ref.bytes, ref.offset, ref.length);
}
- return rsa.verify(signedContent)
- && Arrays.equals(Base64.getEncoder().encode(encryptedPublicKeyData), signatureHash);
+ return rsa.verify(signedContent);
} catch (IOException | NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
throw new IllegalStateException(e);
} finally {
- Arrays.fill(encryptedPublicKeyData, (byte) 0);
if (signedContent != null) {
Arrays.fill(signedContent, (byte) 0);
}
- if (signatureHash != null) {
- Arrays.fill(signatureHash, (byte) 0);
- }
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/SelfGeneratedLicense.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/SelfGeneratedLicense.java
index 7ec6b0b95eb..0bc49d517cd 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/SelfGeneratedLicense.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/SelfGeneratedLicense.java
@@ -19,25 +19,36 @@ import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.Collections;
-import static org.elasticsearch.license.CryptUtils.decrypt;
+import static org.elasticsearch.license.CryptUtils.encryptV3Format;
import static org.elasticsearch.license.CryptUtils.encrypt;
+import static org.elasticsearch.license.CryptUtils.decryptV3Format;
+import static org.elasticsearch.license.CryptUtils.decrypt;
class SelfGeneratedLicense {
public static License create(License.Builder specBuilder) {
+ return create(specBuilder, License.VERSION_CURRENT);
+ }
+
+ public static License create(License.Builder specBuilder, int version) {
License spec = specBuilder
.issuer("elasticsearch")
- .version(License.VERSION_CURRENT)
+ .version(version)
.build();
final String signature;
try {
XContentBuilder contentBuilder = XContentFactory.contentBuilder(XContentType.JSON);
spec.toXContent(contentBuilder, new ToXContent.MapParams(Collections.singletonMap(License.LICENSE_SPEC_VIEW_MODE, "true")));
- byte[] encrypt = encrypt(BytesReference.toBytes(BytesReference.bytes(contentBuilder)));
+ byte[] encrypt;
+ if (version < License.VERSION_CRYPTO_ALGORITHMS) {
+ encrypt = encryptV3Format(BytesReference.toBytes(BytesReference.bytes(contentBuilder)));
+ } else {
+ encrypt = encrypt(BytesReference.toBytes(BytesReference.bytes(contentBuilder)));
+ }
byte[] bytes = new byte[4 + 4 + encrypt.length];
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
- // always generate license version -VERSION_CURRENT
- byteBuffer.putInt(-License.VERSION_CURRENT)
+ // Set -version in signature
+ byteBuffer.putInt(-version)
.putInt(encrypt.length)
.put(encrypt);
signature = Base64.getEncoder().encodeToString(bytes);
@@ -56,9 +67,11 @@ class SelfGeneratedLicense {
byte[] content = new byte[contentLen];
byteBuffer.get(content);
final License expectedLicense;
+ // Version in signature is -version, so check for -(-version) < 4
+ byte[] decryptedContent = (-version < License.VERSION_CRYPTO_ALGORITHMS) ? decryptV3Format(content) : decrypt(content);
// EMPTY is safe here because we don't call namedObject
try (XContentParser parser = XContentFactory.xContent(XContentType.JSON)
- .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, decrypt(content))) {
+ .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, decryptedContent)) {
parser.nextToken();
expectedLicense = License.builder().fromLicenseSpec(License.fromXContent(parser),
license.signature()).version(-version).build();
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java
index ef654513c80..77695f64538 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java
@@ -58,15 +58,41 @@ public class StartupSelfGeneratedLicenseTask extends ClusterStateUpdateTask {
throw new IllegalArgumentException("Illegal self generated license type [" + type +
"]. Must be trial or basic.");
}
-
return updateWithLicense(currentState, type);
} else if (LicenseUtils.licenseNeedsExtended(currentLicensesMetaData.getLicense())) {
return extendBasic(currentState, currentLicensesMetaData);
+ } else if (LicenseUtils.signatureNeedsUpdate(currentLicensesMetaData.getLicense())) {
+ return updateLicenseSignature(currentState, currentLicensesMetaData);
} else {
return currentState;
}
}
+ private ClusterState updateLicenseSignature(ClusterState currentState, LicensesMetaData currentLicenseMetaData) {
+ License license = currentLicenseMetaData.getLicense();
+ MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
+ String type = license.type();
+ long issueDate = license.issueDate();
+ long expiryDate;
+ if ("basic".equals(type)) {
+ expiryDate = LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
+ } else {
+ expiryDate = issueDate + LicenseService.NON_BASIC_SELF_GENERATED_LICENSE_DURATION.getMillis();
+ }
+ License.Builder specBuilder = License.builder()
+ .uid(license.uid())
+ .issuedTo(license.issuedTo())
+ .maxNodes(selfGeneratedLicenseMaxNodes)
+ .issueDate(issueDate)
+ .type(type)
+ .expiryDate(expiryDate);
+ License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder);
+ Version trialVersion = currentLicenseMetaData.getMostRecentTrialVersion();
+ LicensesMetaData newLicenseMetadata = new LicensesMetaData(selfGeneratedLicense, trialVersion);
+ mdBuilder.putCustom(LicensesMetaData.TYPE, newLicenseMetadata);
+ return ClusterState.builder(currentState).metaData(mdBuilder).build();
+ }
+
@Override
public void onFailure(String source, @Nullable Exception e) {
logger.error((Supplier>) () -> new ParameterizedMessage("unexpected failure during [{}]", source), e);
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceClusterTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceClusterTests.java
index 6bf2befbddd..a1a709a9d8f 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceClusterTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceClusterTests.java
@@ -153,6 +153,27 @@ public class LicenseServiceClusterTests extends AbstractLicensesIntegrationTestC
assertLicenseActive(false);
}
+ public void testClusterRestartWithOldSignature() throws Exception {
+ wipeAllLicenses();
+ internalCluster().startNode();
+ ensureGreen();
+ assertLicenseActive(true);
+ putLicense(TestUtils.generateSignedLicenseOldSignature());
+ LicensingClient licensingClient = new LicensingClient(client());
+ assertThat(licensingClient.prepareGetLicense().get().license().version(), equalTo(License.VERSION_START_DATE));
+ logger.info("--> restart node");
+ internalCluster().fullRestart(); // restart so that license is updated
+ ensureYellow();
+ logger.info("--> await node for enabled");
+ assertLicenseActive(true);
+ licensingClient = new LicensingClient(client());
+ assertThat(licensingClient.prepareGetLicense().get().license().version(), equalTo(License.VERSION_CURRENT)); //license updated
+ internalCluster().fullRestart(); // restart once more and verify updated license is active
+ ensureYellow();
+ logger.info("--> await node for enabled");
+ assertLicenseActive(true);
+ }
+
private void assertOperationMode(License.OperationMode operationMode) throws InterruptedException {
boolean success = awaitBusy(() -> {
for (XPackLicenseState licenseState : internalCluster().getDataNodeInstances(XPackLicenseState.class)) {
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/SelfGeneratedLicenseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/SelfGeneratedLicenseTests.java
index bf480797814..aa27dbdcb49 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/SelfGeneratedLicenseTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/SelfGeneratedLicenseTests.java
@@ -19,7 +19,7 @@ import java.util.Base64;
import java.util.Collections;
import java.util.UUID;
-import static org.elasticsearch.license.CryptUtils.encrypt;
+import static org.elasticsearch.license.CryptUtils.encryptV3Format;
import static org.hamcrest.Matchers.equalTo;
@@ -98,7 +98,7 @@ public class SelfGeneratedLicenseTests extends ESTestCase {
try {
XContentBuilder contentBuilder = XContentFactory.contentBuilder(XContentType.JSON);
spec.toXContent(contentBuilder, new ToXContent.MapParams(Collections.singletonMap(License.LICENSE_SPEC_VIEW_MODE, "true")));
- byte[] encrypt = encrypt(BytesReference.toBytes(BytesReference.bytes(contentBuilder)));
+ byte[] encrypt = encryptV3Format(BytesReference.toBytes(BytesReference.bytes(contentBuilder)));
byte[] bytes = new byte[4 + 4 + encrypt.length];
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.putInt(-spec.version())
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
index e8fa6f32a9b..d236dacaa4d 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
@@ -209,12 +209,11 @@ public class TestUtils {
this.maxNodes = maxNodes;
}
}
-
- public static Path getTestPriKeyPath() throws Exception {
+ private static Path getTestPriKeyPath() throws Exception {
return getResourcePath("/private.key");
}
- public static Path getTestPubKeyPath() throws Exception {
+ private static Path getTestPubKeyPath() throws Exception {
return getResourcePath("/public.key");
}
@@ -244,6 +243,19 @@ public class TestUtils {
return generateSignedLicense(type, randomIntBetween(License.VERSION_START, License.VERSION_CURRENT), issueDate, expiryDuration);
}
+ public static License generateSignedLicenseOldSignature() {
+ long issueDate = System.currentTimeMillis();
+ License.Builder specBuilder = License.builder()
+ .uid(UUID.randomUUID().toString())
+ .version(License.VERSION_START_DATE)
+ .issuedTo("customer")
+ .maxNodes(5)
+ .type("trial")
+ .issueDate(issueDate)
+ .expiryDate(issueDate + TimeValue.timeValueHours(24).getMillis());
+ return SelfGeneratedLicense.create(specBuilder, License.VERSION_START_DATE);
+ }
+
/**
* This method which chooses the license type randomly if the type is null. However, it will not randomly
* choose trial or basic types as those types can only be self-generated.
@@ -269,7 +281,7 @@ public class TestUtils {
builder.subscriptionType((type != null) ? type : randomFrom("dev", "gold", "platinum", "silver"));
builder.feature(randomAlphaOfLength(10));
}
- LicenseSigner signer = new LicenseSigner(getTestPriKeyPath(), getTestPubKeyPath());
+ final LicenseSigner signer = new LicenseSigner(getTestPriKeyPath(), getTestPubKeyPath());
return signer.sign(builder.build());
}
diff --git a/x-pack/plugin/core/src/test/resources/private.key b/x-pack/plugin/core/src/test/resources/private.key
index 1f545803d87..d0a658c3f07 100644
Binary files a/x-pack/plugin/core/src/test/resources/private.key and b/x-pack/plugin/core/src/test/resources/private.key differ
diff --git a/x-pack/plugin/core/src/test/resources/public.key b/x-pack/plugin/core/src/test/resources/public.key
index 2a9f272e0b3..a94823f5b62 100644
Binary files a/x-pack/plugin/core/src/test/resources/public.key and b/x-pack/plugin/core/src/test/resources/public.key differ
diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml
index 9eb3b79fda7..8d17e0184af 100644
--- a/x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml
+++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml
@@ -4,7 +4,7 @@ teardown:
xpack.license.post:
acknowledge: true
body: |
- {"licenses":[{"uid":"894371dc-9t49-4997-93cb-8o2e3r7fa6a8","type":"trial","issue_date_in_millis":1411948800000,"expiry_date_in_millis":1916956799999,"max_nodes":1,"issued_to":"issuedTo","issuer":"issuer","signature":"AAAAAgAAAA0FWh0T9njItjQ2qammAAABmC9ZN0hjZDBGYnVyRXpCOW5Bb3FjZDAxOWpSbTVoMVZwUzRxVk1PSmkxakxZdW5IMlhlTHNoN1N2MXMvRFk4d3JTZEx3R3RRZ0pzU3lobWJKZnQvSEFva0ppTHBkWkprZWZSQi9iNmRQNkw1SlpLN0lDalZCS095MXRGN1lIZlpYcVVTTnFrcTE2dzhJZmZrdFQrN3JQeGwxb0U0MXZ0dDJHSERiZTVLOHNzSDByWnpoZEphZHBEZjUrTVBxRENNSXNsWWJjZllaODdzVmEzUjNiWktNWGM5TUhQV2plaUo4Q1JOUml4MXNuL0pSOEhQaVB2azhmUk9QVzhFeTFoM1Q0RnJXSG53MWk2K055c28zSmRnVkF1b2JSQkFLV2VXUmVHNDZ2R3o2VE1qbVNQS2lxOHN5bUErZlNIWkZSVmZIWEtaSU9wTTJENDVvT1NCYklacUYyK2FwRW9xa0t6dldMbmMzSGtQc3FWOTgzZ3ZUcXMvQkt2RUZwMFJnZzlvL2d2bDRWUzh6UG5pdENGWFRreXNKNkE9PQAAAQBZhvozA0trrxhUZ1QbaTsKTna9C5KVQ6pv8yg1pnsBpZXCl8kX1SrgoFn1bXq61IvJwfw5qnmYNiH3hRhTO9EyaCBqaLk8NXZQ6TrRkQSpEnnBwAYUkZeKXsIuBoOk4B4mzwC/r8aMAkzrTiEBtBbog+57cSaU9y37Gkdd+1jXCQrxP+jOEUf7gnXWZvE6oeRroLvCt1fYn09k0CF8kKTbrPTSjC6igZR3uvTHyee74XQ9PRavvHax73T4UOEdQZX/P1ibSQIWKbBRD5YQ1POYVjTayoltTnWLMxfEcAkkATJZLhpBEHST7kZWjrTS6J1dCReJc7a8Vsj/78HXvOIy"}]}
+ {"licenses":[{"uid":"3aa62ffe-36e1-4fad-bfdc-9dff8301eb22","type":"trial","issue_date_in_millis":1523456691721,"expiry_date_in_millis":1838816691721,"max_nodes":5,"issued_to":"customer","issuer":"elasticsearch","signature":"AAAABAAAAA2kWNcuc+DT0lrlmYZKAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAEn6fG9y2VxKBu2T3D5hffh56kzOQODCOdhr0y2d17ZSIJMZRqO7ZywPCWNS1aR33GhfIHkTER0ysML0xMH/gXavhyRvMBndJj0UBKzuwpTawSlnxYtcqN8mSBIvJC7Ki+uJ1SpAILC2ZP9fnkRlqwXqBlTwfYn7xnZgu9DKrOWru/ipTPObo7jcePl8VTK6nWFen7/hCFDQTUFZ0jQvd+nq7A1PAcHGNxGfdbMVmAXCXgGWkRfT3clo9/vadgo+isNyh1sPq9mN7gwsvBAKtA1FrpH2EXYYbfOsSpBvUmhYMgErLg1k3/CbS0pCWLKOaX1xTMayosdZOjagU3auZXY=","start_date_in_millis":-1}]}
---
"Installing and getting license works":