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":