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:
parent
3e9fe3c9cd
commit
cca1a2a7cf
|
@ -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.
Binary file not shown.
Binary file not shown.
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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":
|
||||
|
||||
|
|
Loading…
Reference in New Issue