Make licensing FIPS-140 compliant (#30251)

Necessary changes so that the licensing functionality can be
used in a JVM in FIPS 140 approved mode.
* Uses adequate salt length in encryption
* Changes key derivation to PBKDF2WithHmacSHA512 from a custom
  approach with SHA512 and manual key stretching
* Removes redundant manual padding

Other relevant changes:
* Uses the SAH512 hash instead of the encrypted key bytes as the
  key fingerprint to be included in the license specification
* Removes the explicit verification check of the encryption key
  as this is implicitly checked in signature verification.
This commit is contained in:
Ioannis Kakavas 2018-05-02 22:19:43 +03:00 committed by GitHub
parent 3e9fe3c9cd
commit cca1a2a7cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 213 additions and 131 deletions

View File

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

Binary file not shown.

View File

@ -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 <code>passPhrase</code>
*/
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 <code>passPhrase</code>
* 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 <code>passPhrase</code>
*/
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 <code>data</code> with <code>DEFAULT_PASS_PHRASE</code>
*/
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 <code>encryptedData</code> with <code>DEFAULT_PASS_PHRASE</code>
*/
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 <code>data</code> with <code>passPhrase</code>
*/
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();
}
}

View File

@ -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

View File

@ -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()) {

View File

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

View File

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

View File

@ -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();

View File

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

View File

@ -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)) {

View File

@ -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())

View File

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

View File

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